@anmol-srv/sigil 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -12
- package/README.md +90 -82
- package/dist/cli.js +758 -507
- package/dist/daemon.js +724 -0
- package/dist/hooks/post-tool-use.js +20 -22
- package/dist/hooks/session-end.js +63 -61
- package/dist/hooks/stop.js +76 -74
- package/dist/hooks/user-prompt-submit.js +55 -47
- package/dist/server.js +45 -554
- package/integrations/hermes/README.md +4 -4
- package/integrations/hermes/plugin/README.md +8 -8
- package/knexfile.js +29 -8
- package/package.json +11 -5
- package/src/db/migrations/20260601000000_create-device-table.cjs +34 -0
- package/src/db/migrations/20260601000001_create-pairing-code-table.cjs +27 -0
- package/src/db/migrations/20260601000002_add-fact-provenance.cjs +31 -0
- package/src/db/migrations/20260601000003_add-device-revoked-reason.cjs +19 -0
- package/src/db/migrations/20260601000004_create-trace-event-table.cjs +35 -0
- package/src/db/migrations/20260601000005_add-fact-agent-provenance.cjs +25 -0
- package/src/gui/web/api.js +37 -0
- package/src/gui/web/app.css +947 -0
- package/src/gui/web/app.js +1230 -0
- package/src/gui/web/components.js +90 -0
- package/src/gui/web/design/colors_and_type.css +178 -0
- package/src/gui/web/design/sigil-mark-mono.svg +8 -0
- package/src/gui/web/design/sigil-mark.svg +26 -0
- package/src/gui/web/index.html +536 -0
- package/src/gui/web/sigil.svg +31 -0
- package/src/gui/web/toast.js +62 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hermes integration
|
|
2
2
|
|
|
3
|
-
Sigil ships as a [Hermes Agent](https://hermes.chat) memory provider plugin. The plugin source lives at [`plugin/`](./plugin)
|
|
3
|
+
Sigil ships as a [Hermes Agent](https://hermes.chat) memory provider plugin. The plugin source lives at [`plugin/`](./plugin), copy that directory into Hermes' plugin tree on whichever machine runs Hermes.
|
|
4
4
|
|
|
5
5
|
## Quick deploy (manual)
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ Restart Hermes. Verify with `hermes memory status` (or whatever Hermes' status c
|
|
|
23
23
|
| Hermes hook | Sigil call | Why |
|
|
24
24
|
|---|---|---|
|
|
25
25
|
| `is_available()` | `which sigil` | Avoid network calls; just check the binary exists. |
|
|
26
|
-
| `initialize(session_id, platform, ...)` | sets namespace = `hermes-<platform>` | Per-platform classification
|
|
26
|
+
| `initialize(session_id, platform, ...)` | sets namespace = `hermes-<platform>` | Per-platform classification, see plugin/README.md. |
|
|
27
27
|
| `prefetch(query)` | `sigil search <q> --namespace=hermes-<platform>,default --limit=5 --no-graph` | Fast cross-namespace recall = the shared brain. |
|
|
28
28
|
| `sync_turn(user, assistant)` | `sigil remember --bg "<user>"` in a daemon thread | Non-blocking. Sigil's classifier decides what's worth keeping. |
|
|
29
29
|
| `get_tool_schemas()` | `sigil_search`, `sigil_remember` | Lets the model explicitly drill down or save mid-turn. |
|
|
@@ -37,5 +37,5 @@ A `src/lib/clients/hermes.js` module (5th client alongside Claude Code / Cursor
|
|
|
37
37
|
## Caveats
|
|
38
38
|
|
|
39
39
|
- **Sigil CLI must be on `PATH`** on whichever machine runs Hermes. If `which sigil` returns nothing, `is_available()` returns false and Hermes silently falls back to its built-in memory.
|
|
40
|
-
- **`~/.sigil/.env` must be configured
|
|
41
|
-
- **The plugin shells out for every prefetch.** Latency is `sigil search` latency. The plugin keeps this path retrieval-only; if Hermes' per-turn budget is tighter, we could move to in-process via a Python<>Node bridge
|
|
40
|
+
- **`~/.sigil/.env` must be configured**, run `sigil init` on the Hermes host before activating the plugin.
|
|
41
|
+
- **The plugin shells out for every prefetch.** Latency is `sigil search` latency. The plugin keeps this path retrieval-only; if Hermes' per-turn budget is tighter, we could move to in-process via a Python<>Node bridge, out of scope for v0.1.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Sigil Memory Provider
|
|
2
2
|
|
|
3
|
-
Persistent memory for Hermes Agent, backed by [Sigil](https://github.com/anmolsrv/sigil)
|
|
3
|
+
Persistent memory for Hermes Agent, backed by [Sigil](https://github.com/anmolsrv/sigil), a local-first knowledge engine with atomic facts, entity graph, and hybrid retrieval. Same memory store used by Claude Code, Cursor, Codex CLI, and Kiro.
|
|
4
4
|
|
|
5
5
|
## Why this exists
|
|
6
6
|
|
|
7
|
-
You're running Hermes on a server (e.g. via iMessage / Telegram / Discord gateway) and you also use Claude Code / Cursor / etc. on your laptop. You want **one brain** that all of them share
|
|
7
|
+
You're running Hermes on a server (e.g. via iMessage / Telegram / Discord gateway) and you also use Claude Code / Cursor / etc. on your laptop. You want **one brain** that all of them share, without copying memories around or rebuilding them per tool.
|
|
8
8
|
|
|
9
9
|
This plugin makes that real: every Hermes turn lands in a Sigil namespace, every laptop turn lands in `default`, and cross-namespace search means anyone can recall anything.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
|
-
- Sigil CLI on `PATH
|
|
13
|
+
- Sigil CLI on `PATH`: `npm install -g @anmol-srv/sigil`
|
|
14
14
|
- `sigil init` completed once (configures DB, embedder, LLM provider)
|
|
15
15
|
- Postgres reachable from this machine (local install or shared via Tailscale / cloud)
|
|
16
16
|
|
|
@@ -20,7 +20,7 @@ This plugin makes that real: every Hermes turn lands in a Sigil namespace, every
|
|
|
20
20
|
hermes config set memory.provider sigil
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
No additional env vars or config files
|
|
23
|
+
No additional env vars or config files; Sigil reads its own `~/.sigil/.env`.
|
|
24
24
|
|
|
25
25
|
## How it classifies sources
|
|
26
26
|
|
|
@@ -51,13 +51,13 @@ sigil namespace list
|
|
|
51
51
|
| `sigil_search` | Drill-down search across this platform + `default`. The model is told to use this only when the auto-injected context didn't surface what it needed. |
|
|
52
52
|
| `sigil_remember` | Explicit save. The model is told to use this only when the user asks ("remember that...") or a critical fact arrives mid-turn. |
|
|
53
53
|
|
|
54
|
-
Routine fact capture happens automatically via `sync_turn
|
|
54
|
+
Routine fact capture happens automatically via `sync_turn`, no model action required.
|
|
55
55
|
|
|
56
56
|
## What lives where
|
|
57
57
|
|
|
58
58
|
| Layer | Where | Owns |
|
|
59
59
|
|---|---|---|
|
|
60
|
-
| This plugin | `~/.hermes/hermes-agent/plugins/memory/sigil/` | The Hermes ABC contract
|
|
60
|
+
| This plugin | `~/.hermes/hermes-agent/plugins/memory/sigil/` | The Hermes ABC contract: initialize, prefetch, sync_turn, tool dispatch. Thin subprocess wrapper. |
|
|
61
61
|
| Sigil CLI | `which sigil` | Hybrid search, fact extraction, AUDM dedup, pod-aware retrieval, embedder calls. |
|
|
62
62
|
| Sigil config | `~/.sigil/.env` | DB connection, embedder choice, LLM provider. Run `sigil init` to reconfigure. |
|
|
63
63
|
| Sigil data | Postgres (`SIGIL_DB_HOST` in `~/.sigil/.env`) | All facts, entities, pods, relations. Shared across machines when they point at the same Postgres. |
|
|
@@ -66,7 +66,7 @@ Routine fact capture happens automatically via `sync_turn` — no model action r
|
|
|
66
66
|
|
|
67
67
|
Point `SIGIL_DB_HOST` in every machine's `~/.sigil/.env` at the *same* Postgres. Two common topologies:
|
|
68
68
|
|
|
69
|
-
1. **Server-hosted Postgres
|
|
70
|
-
2. **Cloud Postgres
|
|
69
|
+
1. **Server-hosted Postgres**: Postgres on this server; laptop connects over Tailscale.
|
|
70
|
+
2. **Cloud Postgres**: Supabase / Neon / RDS; both machines connect to it.
|
|
71
71
|
|
|
72
72
|
Either way: one DB, many writers, every namespace visible from everywhere.
|
package/knexfile.js
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
import 'dotenv
|
|
1
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
import { buildLocalConnection } from './src/db/drivers/local-postgres.js';
|
|
7
|
+
import { buildUrlConnection } from './src/db/drivers/url.js';
|
|
8
|
+
|
|
9
|
+
// Env precedence: shell env > project .env > global ~/.sigil/.env.
|
|
10
|
+
// Matches the CLI's loader so `npx knex migrate:latest` Just Works.
|
|
11
|
+
const projectEnv = resolve(process.cwd(), '.env');
|
|
12
|
+
const globalEnv = join(homedir(), '.sigil', '.env');
|
|
13
|
+
if (existsSync(projectEnv)) dotenvConfig({ path: projectEnv, quiet: true });
|
|
14
|
+
if (existsSync(globalEnv) && globalEnv !== projectEnv) dotenvConfig({ path: globalEnv, quiet: true });
|
|
2
15
|
|
|
3
16
|
const env = (key, fallback) => process.env[key] ?? fallback;
|
|
4
17
|
|
|
18
|
+
const url = env('SIGIL_DATABASE_URL', env('DATABASE_URL', ''));
|
|
19
|
+
|
|
20
|
+
const connection = url
|
|
21
|
+
? buildUrlConnection(url)
|
|
22
|
+
: buildLocalConnection({
|
|
23
|
+
db: {
|
|
24
|
+
host: env('SIGIL_DB_HOST', 'localhost'),
|
|
25
|
+
port: Number(env('SIGIL_DB_PORT', 5432)),
|
|
26
|
+
database: env('SIGIL_DB_NAME', 'sigil'),
|
|
27
|
+
user: env('SIGIL_DB_USER', 'sigil_app'),
|
|
28
|
+
password: env('SIGIL_DB_PASSWORD', ''),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
5
32
|
export default {
|
|
6
33
|
client: 'pg',
|
|
7
|
-
connection
|
|
8
|
-
host: env('SIGIL_DB_HOST', 'localhost'),
|
|
9
|
-
port: Number(env('SIGIL_DB_PORT', 5432)),
|
|
10
|
-
database: env('SIGIL_DB_NAME', 'sigil'),
|
|
11
|
-
user: env('SIGIL_DB_USER', 'sigil_app'),
|
|
12
|
-
password: env('SIGIL_DB_PASSWORD', ''),
|
|
13
|
-
},
|
|
34
|
+
connection,
|
|
14
35
|
migrations: { directory: './src/db/migrations' },
|
|
15
36
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anmol-srv/sigil",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Local-first memory infrastructure for AI coding agents. One brain shared across Claude Code, Codex CLI, Cursor, Kiro, Continue, Cline, Windsurf
|
|
5
|
+
"description": "Local-first memory infrastructure for AI coding agents. One brain shared across Claude Code, Codex CLI, Cursor, Kiro, Continue, Cline, Windsurf, or any MCP client. Organized in pluggable pods, stored in your own Postgres. No cloud, no telemetry. Auto-captured from Claude Code via hooks; surfaced everywhere else as a 9-tool MCP server.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sigil": "dist/cli.js"
|
|
8
8
|
},
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"prepublishOnly": "npm run build",
|
|
17
17
|
"migrate": "knex migrate:latest",
|
|
18
18
|
"migrate:rollback": "knex migrate:rollback",
|
|
19
|
-
"migrate:make": "knex migrate:make"
|
|
19
|
+
"migrate:make": "knex migrate:make",
|
|
20
|
+
"test:reliability": "node test/reliability/run.js",
|
|
21
|
+
"db:test:up": "docker compose up -d db",
|
|
22
|
+
"db:test:down": "docker compose down"
|
|
20
23
|
},
|
|
21
24
|
"engines": {
|
|
22
25
|
"node": ">=20.0.0"
|
|
@@ -25,6 +28,7 @@
|
|
|
25
28
|
"dist/",
|
|
26
29
|
"prompts/",
|
|
27
30
|
"src/db/migrations/",
|
|
31
|
+
"src/gui/web/",
|
|
28
32
|
"integrations/",
|
|
29
33
|
"knexfile.js",
|
|
30
34
|
"LICENSE",
|
|
@@ -67,7 +71,7 @@
|
|
|
67
71
|
"local-first"
|
|
68
72
|
],
|
|
69
73
|
"author": "Anmol Srivastava",
|
|
70
|
-
"license": "
|
|
74
|
+
"license": "MIT",
|
|
71
75
|
"repository": {
|
|
72
76
|
"type": "git",
|
|
73
77
|
"url": "https://github.com/Anmol-Srv/sigil.git"
|
|
@@ -79,9 +83,11 @@
|
|
|
79
83
|
"dependencies": {
|
|
80
84
|
"@iarna/toml": "^2.2.5",
|
|
81
85
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
86
|
+
"@number0/iroh": "^0.35.0",
|
|
82
87
|
"dotenv": "^17.3.1",
|
|
83
88
|
"knex": "^3.1.0",
|
|
84
|
-
"pg": "^8.20.0"
|
|
89
|
+
"pg": "^8.20.0",
|
|
90
|
+
"ws": "^8.21.0"
|
|
85
91
|
},
|
|
86
92
|
"optionalDependencies": {
|
|
87
93
|
"@anthropic-ai/sdk": "^0.30.0"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* device — devices that have paired with this Sigil cluster.
|
|
3
|
+
*
|
|
4
|
+
* Each row represents a remote Sigil install whose owner has been
|
|
5
|
+
* authorized to read/write this device's memory. `node_id` is the
|
|
6
|
+
* 64-char hex Ed25519 public key (Iroh NodeID + identity, same value).
|
|
7
|
+
*
|
|
8
|
+
* Roles:
|
|
9
|
+
* reader — can call read-side RPC methods only
|
|
10
|
+
* writer — can also remember/forget/ingest
|
|
11
|
+
* admin — can manage other devices (create pairing codes, revoke)
|
|
12
|
+
*
|
|
13
|
+
* `namespaces` scopes what this device can read/write. Empty array means
|
|
14
|
+
* "all namespaces this cluster knows about" (admin default).
|
|
15
|
+
*
|
|
16
|
+
* `active=false` is a soft revoke — preserves history of the device for
|
|
17
|
+
* audit purposes without allowing further calls.
|
|
18
|
+
*/
|
|
19
|
+
exports.up = (knex) =>
|
|
20
|
+
knex.schema.createTable('device', (t) => {
|
|
21
|
+
t.increments('id').primary();
|
|
22
|
+
t.text('node_id').notNullable().unique();
|
|
23
|
+
t.text('name').notNullable();
|
|
24
|
+
t.text('role').notNullable().defaultTo('writer'); // reader | writer | admin
|
|
25
|
+
t.specificType('namespaces', 'text[]').notNullable().defaultTo('{}');
|
|
26
|
+
t.boolean('active').notNullable().defaultTo(true);
|
|
27
|
+
t.jsonb('meta').notNullable().defaultTo('{}');
|
|
28
|
+
t.timestamp('last_seen_at');
|
|
29
|
+
t.timestamps(false, true);
|
|
30
|
+
|
|
31
|
+
t.index(['node_id', 'active'], 'idx_device_lookup');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
exports.down = (knex) => knex.schema.dropTable('device');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pairing_code — one-shot, time-limited tokens that authorize a new
|
|
3
|
+
* device to register itself.
|
|
4
|
+
*
|
|
5
|
+
* The plaintext code is shown to the operator (printed by `sigil pair
|
|
6
|
+
* create`); only its SHA-256 hash is stored. Joining device presents
|
|
7
|
+
* the plaintext during the pairing handshake.
|
|
8
|
+
*
|
|
9
|
+
* `consumed_by_device_id` is set once the code is redeemed; thereafter
|
|
10
|
+
* the row is kept for audit but cannot be redeemed again.
|
|
11
|
+
*/
|
|
12
|
+
exports.up = (knex) =>
|
|
13
|
+
knex.schema.createTable('pairing_code', (t) => {
|
|
14
|
+
t.increments('id').primary();
|
|
15
|
+
t.text('code_hash').notNullable().unique();
|
|
16
|
+
t.text('name').notNullable(); // intended device name, e.g. "laptop-b"
|
|
17
|
+
t.text('role').notNullable().defaultTo('writer'); // role to assign on redemption
|
|
18
|
+
t.specificType('namespaces', 'text[]').notNullable().defaultTo('{}');
|
|
19
|
+
t.timestamp('expires_at').notNullable();
|
|
20
|
+
t.integer('consumed_by_device_id').references('device.id').onDelete('SET NULL');
|
|
21
|
+
t.timestamp('consumed_at');
|
|
22
|
+
t.timestamps(false, true);
|
|
23
|
+
|
|
24
|
+
t.index(['expires_at'], 'idx_pairing_code_expiry');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
exports.down = (knex) => knex.schema.dropTable('pairing_code');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add cross-device provenance + embedding-shape columns to fact.
|
|
3
|
+
*
|
|
4
|
+
* embedding_model — the model that produced this fact's vector.
|
|
5
|
+
* Cross-device sync refuses if the master's manifest
|
|
6
|
+
* says a different model is in use.
|
|
7
|
+
* embedding_dim — dimensionality of the vector. Belt-and-braces
|
|
8
|
+
* alongside the model check.
|
|
9
|
+
* created_by_device_id — which paired device wrote this fact. NULL means
|
|
10
|
+
* "the local install" (back-compat with rows that
|
|
11
|
+
* existed before this migration).
|
|
12
|
+
*
|
|
13
|
+
* The columns are nullable so the migration is backfill-free; new ingests
|
|
14
|
+
* populate them from the embedder config + the authenticated caller's
|
|
15
|
+
* device row.
|
|
16
|
+
*/
|
|
17
|
+
exports.up = (knex) =>
|
|
18
|
+
knex.schema.alterTable('fact', (t) => {
|
|
19
|
+
t.text('embedding_model');
|
|
20
|
+
t.integer('embedding_dim');
|
|
21
|
+
t.integer('created_by_device_id').references('device.id').onDelete('SET NULL');
|
|
22
|
+
t.index(['created_by_device_id'], 'idx_fact_by_device');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
exports.down = (knex) =>
|
|
26
|
+
knex.schema.alterTable('fact', (t) => {
|
|
27
|
+
t.dropIndex(['created_by_device_id'], 'idx_fact_by_device');
|
|
28
|
+
t.dropColumn('created_by_device_id');
|
|
29
|
+
t.dropColumn('embedding_dim');
|
|
30
|
+
t.dropColumn('embedding_model');
|
|
31
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distinguishes "paused" revokes (a key out for service / on a borrowed
|
|
3
|
+
* laptop, expected to come back) from "compromised" revokes (terminal —
|
|
4
|
+
* the keypair leaked and must never authenticate again).
|
|
5
|
+
*
|
|
6
|
+
* 'paused' → device.activate flips active=true (default)
|
|
7
|
+
* 'compromised' → device.activate refuses; requires re-pairing
|
|
8
|
+
*
|
|
9
|
+
* Nullable so existing rows are unaffected.
|
|
10
|
+
*/
|
|
11
|
+
exports.up = (knex) =>
|
|
12
|
+
knex.schema.alterTable('device', (t) => {
|
|
13
|
+
t.text('revoked_reason'); // 'paused' | 'compromised' | NULL
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
exports.down = (knex) =>
|
|
17
|
+
knex.schema.alterTable('device', (t) => {
|
|
18
|
+
t.dropColumn('revoked_reason');
|
|
19
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trace_event — persisted, queryable causal log of what the daemon did.
|
|
3
|
+
*
|
|
4
|
+
* Each row is one top-level operation (a search, an ingest/remember, a
|
|
5
|
+
* lifecycle sweep). `detail` (jsonb) holds the full structured trace:
|
|
6
|
+
* for search — the routing decision, matched entity, and every ranked
|
|
7
|
+
* fact with its similarity / RRF / ACT-R activation (decay) / final
|
|
8
|
+
* score; for ingest — classify → chunk → extract → per-fact AUDM verdict
|
|
9
|
+
* (with the similarity that drove it) → entity links.
|
|
10
|
+
*
|
|
11
|
+
* The live activity feed still streams over the event bus; this table is
|
|
12
|
+
* the durable history the GUI's Activity tab reads + filters.
|
|
13
|
+
*/
|
|
14
|
+
exports.up = async (knex) => {
|
|
15
|
+
await knex.schema.createTable('trace_event', (t) => {
|
|
16
|
+
t.bigIncrements('id').primary();
|
|
17
|
+
t.text('uid').notNullable().unique(); // trace-<nanoid>
|
|
18
|
+
t.text('kind').notNullable(); // 'search' | 'ingest' | 'lifecycle' | ...
|
|
19
|
+
t.timestamp('ts', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
|
20
|
+
t.integer('duration_ms');
|
|
21
|
+
t.text('namespace');
|
|
22
|
+
t.text('summary'); // one-line human description
|
|
23
|
+
t.text('device_id'); // provenance (null = this device)
|
|
24
|
+
t.text('transport'); // cli | mcp | gui | iroh | null
|
|
25
|
+
t.jsonb('detail').notNullable().defaultTo('{}');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Hot path is "latest N, optionally filtered by kind".
|
|
29
|
+
await knex.schema.alterTable('trace_event', (t) => {
|
|
30
|
+
t.index(['ts'], 'trace_event_ts_idx');
|
|
31
|
+
t.index(['kind', 'ts'], 'trace_event_kind_ts_idx');
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
exports.down = (knex) => knex.schema.dropTableIfExists('trace_event');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add `created_by_agent` provenance to fact.
|
|
3
|
+
*
|
|
4
|
+
* created_by_agent — which agent wrote this fact: 'claude-code', 'codex',
|
|
5
|
+
* 'cursor', 'mcp', 'cli', etc. NULL means unknown /
|
|
6
|
+
* pre-migration (back-compat).
|
|
7
|
+
*
|
|
8
|
+
* This is PROVENANCE, not SCOPE: it is recorded, surfaced, and filterable,
|
|
9
|
+
* but never a default retrieval partition. Cross-agent sharing is the product
|
|
10
|
+
* — Claude must still see what Cursor wrote — so agent never enters the
|
|
11
|
+
* default WHERE clause. Nullable = backfill-free; new ingests populate it from
|
|
12
|
+
* the authenticated caller's request-context (AsyncLocalStorage). Mirrors the
|
|
13
|
+
* created_by_device_id column added in 20260601000002.
|
|
14
|
+
*/
|
|
15
|
+
exports.up = (knex) =>
|
|
16
|
+
knex.schema.alterTable('fact', (t) => {
|
|
17
|
+
t.text('created_by_agent');
|
|
18
|
+
t.index(['created_by_agent'], 'idx_fact_by_agent');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
exports.down = (knex) =>
|
|
22
|
+
knex.schema.alterTable('fact', (t) => {
|
|
23
|
+
t.dropIndex(['created_by_agent'], 'idx_fact_by_agent');
|
|
24
|
+
t.dropColumn('created_by_agent');
|
|
25
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central RPC client — the one door from the GUI to the daemon. Mirrors the
|
|
3
|
+
* cohort-live-web axios-wrapper convention: a single place that adds context
|
|
4
|
+
* and turns a structured daemon error ({code,message,hint}) into a toast.
|
|
5
|
+
*
|
|
6
|
+
* Pass { quiet: true } to suppress the auto-toast (e.g. when a caller renders
|
|
7
|
+
* the error inline). The thrown Error carries .code/.hint for callers.
|
|
8
|
+
*/
|
|
9
|
+
import { toast } from './toast.js';
|
|
10
|
+
|
|
11
|
+
export async function rpc(method, params = {}, { quiet = false } = {}) {
|
|
12
|
+
let body;
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch('/api/v1/rpc', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
credentials: 'same-origin',
|
|
18
|
+
body: JSON.stringify({ method, params }),
|
|
19
|
+
});
|
|
20
|
+
body = await res.json();
|
|
21
|
+
} catch {
|
|
22
|
+
const e = {
|
|
23
|
+
code: 'NETWORK',
|
|
24
|
+
message: 'Could not reach the Sigil daemon.',
|
|
25
|
+
hint: 'Is it running? Try `sigil daemon status`.',
|
|
26
|
+
};
|
|
27
|
+
if (!quiet) toast({ variant: 'error', message: e.message, hint: e.hint, code: e.code });
|
|
28
|
+
throw Object.assign(new Error(e.message), e);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!body || body.ok !== true) {
|
|
32
|
+
const e = body?.error || { code: 'UNKNOWN', message: 'request failed' };
|
|
33
|
+
if (!quiet) toast({ variant: 'error', message: e.message, hint: e.hint, code: e.code });
|
|
34
|
+
throw Object.assign(new Error(e.message || 'request failed'), e);
|
|
35
|
+
}
|
|
36
|
+
return body.data;
|
|
37
|
+
}
|