Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ the repository-wide strict-client output-schema contract test.

`note_recall` is a read-only, host-triggered tool for Codex's synchronous
`UserPromptSubmit` hook. It offers bounded historical candidates by numeric
short ID; it does not read note bodies or write notes. The Codex hook installer
is `flicknote hook install codex [--local|--global]`. It resolves the existing
FlickNote MCP registration from Codex TOML, preserves unrelated configuration,
and must not start the daemon or modify hook trust. Failures leave the host
conversation usable without fabricated context; use `note_get` to inspect a
candidate and verify it before any separately authorized edit.
short ID from literal matches of complete extracted topic names and entity
names; it does not read note bodies or write notes. Multiword values are not
split, and the matching rules do not translate, alias, stem, or infer semantic
relationships. The Codex hook installer is `flicknote hook install codex
[--local|--global]`. It resolves the existing FlickNote MCP registration from
Codex TOML, preserves unrelated configuration, and must not start the daemon or
modify hook trust. Failures leave the host conversation usable without
fabricated context; use `note_get` to inspect a candidate and verify it before
any separately authorized edit.


## Build & Test
Expand Down
12 changes: 12 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@ Language for connecting the current conversation to existing notes.
A named person, company, location, or product identified in a note.
_Avoid_: Keyword, topic

**Topic**:
A subject assigned to a note, such as Memory Systems or Knowledge Management. It describes what the note concerns, rather than naming a person, company, location, or product.
_Avoid_: Entity, named object

**Recall query**:
The current message text used to look for connections to existing notes. It expresses the current need, but need not contain the names or subjects recorded on those notes.
_Avoid_: Extracted entity, answer

**Entity recall**:
Finding candidate notes when the current message contains the name of an entity associated with those notes. A match suggests a possible connection, not that the note answers the message.
_Avoid_: Semantic search, answer retrieval

**Topic recall**:
Finding candidate notes when the current message contains a subject associated with those notes. A match suggests a possible connection, not that the note answers the message.
_Avoid_: Semantic search, answer retrieval

**Recall candidate**:
An existing note offered for possible further reading, represented by its identifier, title, available summary, and modification time. It is historical material whose relevance and claims still need evaluation.
_Avoid_: Verified fact, instruction
Expand Down
35 changes: 21 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Daemon-backed note management CLI with local-first sync. The CLI and MCP server
- **Get note details** — retrieve by numeric short ID; view heading structure with `--tree`
- **Edit notes** — human editor, append, content, and metadata workflows; structured content and section mutations are provided by MCP
- **MCP server** — typed local note, source, and project tools over stdio
- **Codex entity recall** — a read-only `UserPromptSubmit` hook that offers bounded historical note candidates before a prompt is sent
- **Codex recall** — a read-only `UserPromptSubmit` hook that offers bounded historical note candidates from extracted topics and entities before a prompt is sent
- **Archive notes** — archive and unarchive
- **Authentication** — email OTP or OAuth (Google/Apple) via Supabase
- **User daemon service** — foreground daemon managed by launchd (macOS) or systemd (Linux)
Expand Down Expand Up @@ -120,7 +120,7 @@ flicknote daemon run
# Reconcile/start the service after an upgrade
flicknote daemon restart

# Install the Codex entity recall hook (choose interactively, or pass a scope)
# Install the Codex recall hook (choose interactively, or pass a scope)
flicknote hook install codex
flicknote hook install codex --global
```
Expand Down Expand Up @@ -167,14 +167,14 @@ start it as a subprocess:
```

The MCP server requires the local daemon. It exposes typed note, discovery,
note-source, project, and read-only entity-recall tools. Note content and exact `before`/`after` edits
note-source, project, and read-only recall tools. Note content and exact `before`/`after` edits
are structured JSON fields, so callers do not need shell heredocs. Note tools
accept numeric short IDs and do not expose internal UUIDs; project tools use
project names. `note_source` reads stored source data, while `note_get` reads
editable note content. Every data tool uses the running daemon; the MCP process
never opens SQLite. The server does not start the daemon automatically.

### Codex entity recall hook
### Codex recall hook

With the FlickNote MCP server already registered in Codex, install the hook with
`flicknote hook install codex`. On a terminal it shows the actual project-local
Expand All @@ -190,16 +190,23 @@ After installation, review and trust the definition in Codex with `/hooks`.
Project-local hooks also require a trusted project. The generated synchronous
`UserPromptSubmit` MCP hook sends the current prompt to `note_recall` with a
one-second timeout. Recall checks the current user's active notes only, matches
literal extracted person/company/location/product names, returns at most five
numeric-ID candidates, and does not read note bodies or generate summaries.
Titles, summaries, and the complete context are bounded; ASCII case-insensitive
matching follows SQLite's behavior, so aliases, semantic matches, and complete
Unicode case folding are not inferred. If the daemon is unavailable or the
recall fails, Codex continues without injected candidates; the hook never writes
notes or starts services implicitly. Use `note_get` with a returned ID to read a
candidate and verify historical information before any independently authorized
edit. See the [Codex hooks documentation](https://developers.openai.com/codex/hooks)
for host trust and MCP hook behavior.
complete stored extracted topic names and person/company/location/product names
literally, returns at most five numeric-ID candidates, and does not read note
bodies or generate summaries. Multiword values are matched as a whole, so
`Memory Systems` matches as a topic while `Memory` does not. A value whose
first or last character is an ASCII letter, digit, or underscore must sit on an
ASCII token edge; for example, `age` does not match `Management`, `age2`, or
`my_age`, but it does
match `(age)` and `用age加密`. Chinese-only values retain substring matching.
Topics are not translated, stemmed, aliased, or split. Matching is ASCII
case-insensitive and literal, so semantic matches and complete Unicode case
folding are not inferred. Titles, summaries, and the complete context are
bounded. If the daemon is unavailable or the recall fails, Codex continues
without injected candidates; the hook never writes notes or starts services
implicitly. Use `note_get` with a returned ID to read a candidate and verify
historical information before any independently authorized edit. See the
[Codex hooks documentation](https://developers.openai.com/codex/hooks) for host
trust and MCP hook behavior.

The Gateway CLI command remains available for internal development and
maintenance requests; it is not the formal agent interface.
Expand Down
15 changes: 4 additions & 11 deletions flicknote-cli/src/commands/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub(crate) struct HookInstallArgs {

#[derive(Subcommand)]
enum HookInstallTarget {
/// Install the read-only entity recall hook for Codex
/// Install the read-only recall hook for Codex
Codex(CodexInstallArgs),
}

Expand Down Expand Up @@ -230,10 +230,7 @@ fn requested_scope(
));
}

writeln!(
output,
"Choose where to install the Codex entity recall hook:"
)?;
writeln!(output, "Choose where to install the Codex recall hook:")?;
writeln!(output, " 1) local {}", paths.local_hooks.display())?;
writeln!(output, " 2) global {}", paths.global_hooks.display())?;
write!(output, "Enter 1 or 2 (blank cancels): ")?;
Expand Down Expand Up @@ -342,16 +339,12 @@ fn print_result(
match result {
InstallResult::Installed { path, updated } => {
let action = if updated { "Updated" } else { "Installed" };
writeln!(
output,
"{action} Codex entity recall hook in {}",
path.display()
)?;
writeln!(output, "{action} Codex recall hook in {}", path.display())?;
}
InstallResult::AlreadyConfigured { locations } => {
writeln!(
output,
"Codex entity recall hook is already configured; no file was changed."
"Codex recall hook is already configured; no file was changed."
)?;
for location in locations {
writeln!(output, " {location}")?;
Expand Down
20 changes: 19 additions & 1 deletion flicknote-cli/src/main_tests/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ async fn seeded_backend(config: &Config) -> (Arc<LocalPowerSyncBackend>, String,
)
.unwrap();
drop(writer);
backend
.set_note_extractions(&no_source_id, "::topic", &["Memory Systems".to_string()])
.await
.unwrap();
let alpha_id = flicknote_core::services::markdown::parse_markdown(
"## Alpha\n\nOld text.\n\n## Beta\n\nKeep me.",
)
Expand Down Expand Up @@ -715,7 +719,7 @@ async fn mcp_discovery_returns_object_wrapped_typed_results() {
assert_eq!(
topics["result"]["structuredContent"],
serde_json::json!({
"topics": ["AI"]
"topics": ["AI", "Memory Systems"]
})
);

Expand Down Expand Up @@ -768,6 +772,20 @@ async fn mcp_recall_returns_hook_context_and_empty_results_without_fabrication()
assert!(context.contains("历史笔记候选结束"));
assert!(!context.contains(&harness.note_uuid));

let topic_recalled = harness
.call(
"note_recall",
serde_json::json!({ "prompt": "Design Memory Systems" }),
)
.await;
assert_eq!(topic_recalled["result"]["isError"], false);
let topic_context =
topic_recalled["result"]["structuredContent"]["hookSpecificOutput"]["additionalContext"]
.as_str()
.unwrap();
assert!(topic_context.contains("\"id\":43"));
assert!(topic_context.contains("No source note"));

let candidate = harness
.call("note_get", serde_json::json!({ "id": 42 }))
.await;
Expand Down
4 changes: 2 additions & 2 deletions flicknote-cli/src/mcp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl FlickNoteMcp {
DaemonClient::new(&self.config).call(request),
)
.await
.map_err(|_| ServiceError::DaemonUnavailable("entity recall timed out".to_string()))?;
.map_err(|_| ServiceError::DaemonUnavailable("recall timed out".to_string()))?;
}
DaemonClient::new(&self.config).call(request).await
}
Expand Down Expand Up @@ -205,7 +205,7 @@ impl FlickNoteMcp {

#[tool(
name = "note_recall",
description = "Recall up to five active notes whose extracted person, company, location, or product entity appears in the prompt. This is read-only host context; use note_get with a returned ID to inspect a candidate.",
description = "Recall up to five active notes whose full extracted topic or person, company, location, or product value appears literally in the prompt (multiword values are not split; values with an ASCII letter, digit, or underscore at an edge use ASCII token edges). This is read-only host context; use note_get with a returned ID to inspect a candidate.",
annotations(read_only_hint = true)
)]
async fn note_recall(
Expand Down
Loading