imprnt 0.1.3-edge.61 → 0.1.3-edge.62

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/CLAUDE.md CHANGED
@@ -148,13 +148,13 @@ imprnt keeps exactly two "robot" helpers, carried over from PAI (the system impr
148
148
 
149
149
  ## Updating & contradictions
150
150
 
151
- Correct the ONE entity note. Everything links by ID so the fix propagates. Old name → `aliases`. A contradiction stamps the stale line `> superseded by [[...]]`. Marked, never silently overwritten. Because `raw/` is immutable, any claim is traceable to its snapshot, and a schema change is just a `reingest` over `raw/`.
151
+ Correct the ONE entity note. Everything links by ID so the fix propagates. Old name → `aliases`. A contradiction stamps the stale line `> superseded by [[...]]`. Marked, never silently overwritten. Because `raw/` is immutable, any claim is traceable to its snapshot, and a schema change is just re-running `ingest` over `raw/`.
152
152
 
153
153
  ## Migrating existing structured knowledge
154
154
 
155
155
  A migration is the one-time bulk WRITE: snapshot every chosen source into `raw/<source>/` (immutable, complete), then the LLM fans each source out into atomic, domain-filed, linked notes. Two rules keep it honest:
156
156
 
157
- - **Re-derive from the ORIGINAL source, not a prior vault.** When you rebuild the vault, go back to the original snapshots in `raw/`. Don't reshuffle a previous vault's notes (that migrates yesterday's mistakes forward). A schema change is just a `reingest` over `raw/`.
157
+ - **Re-derive from the ORIGINAL source, not a prior vault.** When you rebuild the vault, go back to the original snapshots in `raw/`. Don't reshuffle a previous vault's notes (that migrates yesterday's mistakes forward). A schema change is just re-running `ingest` over `raw/`.
158
158
  - **But preserve genuine prior enrichment.** When a *source* is already an atomic, enriched note (a prior system's knowledge notes), map its fields onto this contract and keep its typed links. Don't re-derive it from scratch (that pays the LLM to downgrade good work). Re-deriving from scratch is for *unstructured* sources (the dense prose blobs).
159
159
 
160
160
  **A dense multi-topic source splits into atomic linked notes: one snapshot, many notes.** Real migration sources are dense blobs covering many objects at once (a health-overview file = the med stack + a health profile + an insurance-critical prescription decision + the doctor-as-a-person). Don't file the whole blob as one note. That loses the entity graph (the doctor never becomes a `people/`, the decision never becomes a linkable note). Instead: split it into one atomic note per object across the right folders (`people/` for the doctor, `holdings/kind:med` for a tracked drug+dose, `health/`/`identity/` for the profile/decision), link them, and keep the single `raw/` snapshot as the shared provenance for all of them. `ingest` writes one snapshot. The LLM fans it out into the right number of notes.
@@ -176,11 +176,11 @@ Core is the vault + `ingest → recall → check`. Everything else (a task-mirro
176
176
  - **Write** single-writer-per-path: a plugin writes only its own folder and **proposes** vault-note changes for `ingest`/you to apply. It never mutates a note. Frontmatter labels are **slug-namespaced** (`whenful.synced`), read-your-own-only. Core ignores unknown keys.
177
177
  - **`recall` searches `vault/` only, forever.** Sibling dirs + `raw/` are outside the corpus by construction. A plugin surfaces into search only by **proposing one low-frequency summary note** (the escape hatch).
178
178
  - **Behavior plugins** are a separate class: ship a fixed-size fragment the **user wires into the agent's system prompt**. The vault never auto-injects. Remove = delete the wire-in. No referee for conflicting fragments, the user owns composition. (The old system imposed. imprnt composes.)
179
- - **Harness plugins** are the third class: they customize Claude Code itself, never the vault (timemachine's PreToolUse snapshot hook, the statusline, a skill). The same plugin folder doubles as a **native Claude Code plugin** (`.claude-plugin/plugin.json` + `hooks/` + `skills/`, Anthropic's format) and/or carries **`imp-settings.json`** (the settings keys Claude only takes via config, e.g. `statusLine`, with `${PLUGIN_DIR}` as the plugin's absolute dir). `imp` carries enabled ones into every session it launches: one `--plugin-dir` per native plugin + one merged `--settings`. Stock `claude` stays stock, nothing is ever written to settings files, `plugin rm` undoes everything.
179
+ - **Harness plugins** are the third class: they customize Claude Code itself, never the vault (timemachine's PreToolUse snapshot hook, the statusline, a skill). The same plugin folder doubles as a **native Claude Code plugin** (`.claude-plugin/plugin.json` + `hooks/` + `skills/`, Anthropic's format) and/or carries **`imp-settings.json`** (the settings keys Claude only takes via config, e.g. `statusLine`, with `${PLUGIN_DIR}` as the plugin's absolute dir). `imp` carries enabled ones into every claude session it launches: one `--plugin-dir` per native plugin + one merged `--settings` (gemini has no host for claude harness plugins, so `imp --gemini` skips them with a warning). Stock `claude` stays stock, nothing is ever written to settings files, `plugin rm` undoes everything.
180
180
  - **Capability modules** are the one bend in "share nothing": a module may *provide* a capability another *consumes* (the session-host brokers an authed-session token to the kleinanzeigen watcher). Bounded so it can't re-bloat core: the consumer copies a tiny client (`sessionToken(site) -> string | null`) over a **localhost broker**, never imports the provider or reads its folder. The invariant that replaces "no cross-plugin reads": **removing a provider degrades a consumer gracefully** (`null` = host down, fall back), never breaks it. This is a third contact surface (vault . agent-context . broker), fenced like the sync edge: deterministic, answer-on-request, auditable (token fingerprint, never the token), never resident. The litmus extends: zero core edits to add/remove a *provider* too, because there is no capability registry in core. Credential-holding modules follow the auth finding: human login once, read-only attach, never copy a profile, never automate the password. Full rules: `plugins/README.md`.
181
181
  - **Secrets are env vars at the plugin's edge.** A plugin calling an external service reads its key from an env var named in its README and fails loud when missing. Keys never land in the vault, the plugin folder, or anything committed. No central secret store.
182
182
  - **Commands only, no forced daemon.** The user schedules sync. Install/remove is manual (README `## Install` / `## Remove` + `rm -rf`), no registry.
183
- - **Core ↔ plugin contact = exactly three convention-based aggregators**, all dumb and uniform: `imprnt check --all` globs `plugins/*/check.js` (each plugin's built artifact), runs each with `node`, reads **exit code only**, forwards stdout verbatim, never parses plugin output. `imprnt ingest --apply` files staged notes the plugins drop in `plugins/*/proposed/`. `imp` turns the enable list into launch flags (`--plugin-dir` per native plugin + one merged `--settings`) for the session it spawns. All discover by filename/dir convention, never by import, never by naming a specific plugin. Fence: core may provide read-only *aggregation* helpers, never write/orchestration. (Not k8s liveness/readiness. Nothing runs, so the only health question is "is the data sound", which is what `check` answers.)
183
+ - **Core ↔ plugin contact = exactly four convention-based contact points**, all dumb and uniform: `imprnt check --all` globs `plugins/*/check.js` (each plugin's built artifact), runs each with `node`, reads **exit code only**, forwards stdout verbatim, never parses plugin output. `imprnt ingest --apply` files staged notes the plugins drop in `plugins/*/proposed/`. `imp` turns the enable list into launch flags (`--plugin-dir` per native plugin + one merged `--settings`) for the session it spawns. `imprnt <module> <command>` dispatches to `plugins/<module>/<module>.js` (stdio inherited, exit code passed through, a built-in subcommand always wins, zero per-module logic). All discover by filename/dir convention, never by import, never by naming a specific plugin. Fence: core may provide read-only *aggregation* helpers, never write/orchestration. (Not k8s liveness/readiness. Nothing runs, so the only health question is "is the data sound", which is what `check` answers.)
184
184
 
185
185
  ## Out of scope (on purpose)
186
186
  No task management. No auto-injected context. No background loop. No self-grading. No MCP/vector/embeddings on the vault. No sensitivity machinery (it's private by being private). **No `out/` zone for deliverables.** A produced artifact (an article, an export) lives in your artifacts dir and a vault note points to it. The vault holds knowledge, not outputs. Every plugin is a self-contained dir you can `rm -rf`. "It belongs" is not a reason to add it. (Plugin-contract out-of-scope list lives in `plugins/README.md`.)
package/README.md CHANGED
@@ -127,7 +127,7 @@ asking ("add the anti-slop plugin"), each a separate `imprnt-plugin-*` package:
127
127
  | `imprnt-plugin-anti-slop` | Rules that keep its prose from reading like AI. |
128
128
  | `imprnt-plugin-whenful` | A local mirror of your [Whenful](https://whenful.com) tasks, shown inline at read. |
129
129
  | `imprnt-plugin-kleinanzeigen` | A watcher for your Kleinanzeigen inbox: regex triage of buyer messages, drafts, a phone digest. You press send. |
130
- | `imprnt-plugin-session-host` | A warm browser holding your logged-in sessions, providing the authed-session capability. You enroll each site once. |
130
+ | `imprnt-plugin-session-host` | A warm browser holding your logged-in sessions, providing the authed-session capability. You enroll each site once. Not yet on npm: install from a repo checkout with `imprnt plugin add session-host --from <dir>`. |
131
131
  | `imprnt-plugin-timemachine` | Snapshots your work before each change so you can recover what the agent breaks. |
132
132
  | `imprnt-plugin-statusline` | A customizable status line: model, branch, context, cost, rate limits, clock. |
133
133
  | `imprnt-plugin-telegram` | Your vault from your phone: text a bot, the answer comes from your notes. |
@@ -164,7 +164,7 @@ assistant that files and recalls for you:
164
164
 
165
165
  The docs are the website, [imprnt.dev](https://imprnt.dev), built straight from markdown in
166
166
  [`site/docs/`](site/docs/) (the single source): getting started, how it works, the model, plugins,
167
- architecture, design decisions, and contributing. Edit a file there and the site updates on the next
167
+ design decisions, and contributing. Edit a file there and the site updates on the next
168
168
  deploy.
169
169
 
170
170
  - [`CLAUDE.md`](CLAUDE.md), the contract your assistant reads inside the vault: note formats, conventions.
@@ -174,8 +174,8 @@ deploy.
174
174
 
175
175
  The engine is built with [Bun](https://bun.sh) and [Turborepo](https://turborepo.com) (dev tools
176
176
  only, never needed by people who use it through their assistant). Clone, `bun install`,
177
- `bun run build`, `bun run test`. The architecture and the build-and-release model are in the
178
- [architecture](https://imprnt.dev/architecture/) and [contributing](https://imprnt.dev/contributing/) docs.
177
+ `bun run build`, `bun run test`. The reasoning behind the design and the build-and-release model are in the
178
+ [design decisions](https://imprnt.dev/design-decisions/) and [contributing](https://imprnt.dev/contributing/) docs.
179
179
 
180
180
  ## License
181
181
 
package/dist/cli.js CHANGED
@@ -737,12 +737,19 @@ function readRegistry() {
737
737
  for (const [k, v] of Object.entries(raw?.vaults ?? {}))
738
738
  if (typeof v === "string")
739
739
  vaults[k] = v;
740
+ const defaultModels = {};
741
+ for (const [k, v] of Object.entries(raw?.defaultModels ?? {}))
742
+ if (typeof v === "string")
743
+ defaultModels[k] = v;
744
+ const passthrough = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
740
745
  return {
746
+ ...passthrough,
741
747
  default: typeof raw?.default === "string" ? raw.default : undefined,
742
748
  vaults,
743
749
  defaultAgent: typeof raw?.defaultAgent === "string" ? raw.defaultAgent : undefined,
744
750
  skipPermissions: typeof raw?.skipPermissions === "boolean" ? raw.skipPermissions : undefined,
745
- defaultModel: typeof raw?.defaultModel === "string" ? raw.defaultModel : undefined
751
+ defaultModel: typeof raw?.defaultModel === "string" ? raw.defaultModel : undefined,
752
+ defaultModels: Object.keys(defaultModels).length ? defaultModels : undefined
746
753
  };
747
754
  } catch {
748
755
  return { vaults: {} };
@@ -787,12 +794,25 @@ function setSkipPermissions(on) {
787
794
  reg.skipPermissions = on;
788
795
  return writeRegistry(reg);
789
796
  }
790
- function readDefaultModel() {
791
- return readRegistry().defaultModel;
797
+ function readDefaultModel(agent) {
798
+ const reg = readRegistry();
799
+ const scoped = reg.defaultModels?.[agent];
800
+ if (scoped !== undefined)
801
+ return scoped;
802
+ return agent === (reg.defaultAgent ?? "claude") ? reg.defaultModel : undefined;
792
803
  }
793
- function setDefaultModel(model) {
804
+ function setDefaultModel(model, agent) {
794
805
  const reg = readRegistry();
795
- reg.defaultModel = model || undefined;
806
+ const models = { ...reg.defaultModels ?? {} };
807
+ const legacyAgent = reg.defaultAgent ?? "claude";
808
+ if (reg.defaultModel && models[legacyAgent] === undefined)
809
+ models[legacyAgent] = reg.defaultModel;
810
+ reg.defaultModel = undefined;
811
+ if (model)
812
+ models[agent] = model;
813
+ else
814
+ delete models[agent];
815
+ reg.defaultModels = Object.keys(models).length ? models : undefined;
796
816
  return writeRegistry(reg);
797
817
  }
798
818
  function writeRegistry(reg) {
@@ -1005,14 +1025,19 @@ function joinFragments(parts) {
1005
1025
 
1006
1026
  `);
1007
1027
  }
1028
+ function flagHead(args) {
1029
+ const term = args.indexOf("--");
1030
+ return term < 0 ? args : args.slice(0, term);
1031
+ }
1008
1032
  function claudeRenderArgs(spec) {
1009
1033
  const systemPrompt = spec.insideProject ? spec.globals : joinFragments([spec.cast, spec.pointer, spec.globals]);
1010
1034
  const harness = spec.pluginsRoot ? harnessFlags(spec.pluginsRoot) : [];
1011
1035
  const extra = spec.addDirs.flatMap((d) => ["--add-dir", d]);
1012
1036
  const lead = [];
1013
- if (spec.skipPermissions && !spec.passthrough.includes("--dangerously-skip-permissions"))
1037
+ const head = flagHead(spec.passthrough);
1038
+ if (spec.skipPermissions && !head.includes("--dangerously-skip-permissions"))
1014
1039
  lead.push("--dangerously-skip-permissions");
1015
- if (spec.model && !spec.passthrough.some((a) => a === "--model" || a.startsWith("--model=")))
1040
+ if (spec.model && !head.some((a) => a === "--model" || a.startsWith("--model=")))
1016
1041
  lead.push("--model", spec.model);
1017
1042
  const pass = lead.length ? [...lead, ...spec.passthrough] : spec.passthrough;
1018
1043
  const body = systemPrompt ? mergeFragment(pass, systemPrompt, extra) : [...pass, ...extra];
@@ -1052,15 +1077,15 @@ function geminiRenderArgs(spec) {
1052
1077
  console.error("imp: gemini does not host claude harness plugins (statusline, timemachine) - skipping them");
1053
1078
  }
1054
1079
  const lead = [];
1080
+ const head = flagHead(spec.passthrough);
1055
1081
  if (spec.skipPermissions) {
1056
- if (!spec.passthrough.includes("--yolo") && !spec.passthrough.includes("-y"))
1082
+ if (!head.includes("--yolo") && !head.includes("-y"))
1057
1083
  lead.push("--yolo");
1058
- if (!spec.passthrough.includes("--skip-trust"))
1084
+ if (!head.includes("--skip-trust"))
1059
1085
  lead.push("--skip-trust");
1060
1086
  }
1061
1087
  const pass = [...spec.passthrough];
1062
- const mTerm = pass.indexOf("--");
1063
- const mEnd = mTerm < 0 ? pass.length : mTerm;
1088
+ const mEnd = flagHead(pass).length;
1064
1089
  const isModelFlag = (a) => a === "-m" || a === "--model" || a.startsWith("-m=") || a.startsWith("--model=");
1065
1090
  const mIdx = pass.slice(0, mEnd).findIndex(isModelFlag);
1066
1091
  if (mIdx >= 0) {
@@ -1182,7 +1207,7 @@ function resolveLaunch(passthrough, config = {}) {
1182
1207
  rest.push(a);
1183
1208
  }
1184
1209
  const name = agentFlag ?? process.env.IMPRNT_AGENT ?? config.agent ?? "claude";
1185
- let backend = backends[name];
1210
+ let backend = Object.hasOwn(backends, name) ? backends[name] : undefined;
1186
1211
  if (!backend) {
1187
1212
  console.error(`imp: unknown agent "${name}" - falling back to claude (valid: ${Object.keys(backends).join(", ")})`);
1188
1213
  backend = claudeBackend;
@@ -2318,6 +2343,13 @@ function hostMemoryStores() {
2318
2343
  }
2319
2344
  }).sort();
2320
2345
  }
2346
+ function memoryIndexHasContent(dir2) {
2347
+ try {
2348
+ return readFileSync13(join14(dir2, "MEMORY.md"), "utf8").split(/\r?\n/).some((l) => l.trim() !== "" && l.trim() !== "# Memory index");
2349
+ } catch {
2350
+ return false;
2351
+ }
2352
+ }
2321
2353
  function syncNeedsReview(lines2) {
2322
2354
  const p = join14(vault4, "_needs-review.md");
2323
2355
  const exists = existsSync12(p);
@@ -2497,10 +2529,18 @@ var init_check = __esm(() => {
2497
2529
  for (const r of uncovered)
2498
2530
  review.push(`- [ ] unclassified snapshot \`${r}\` — no vault note points back`);
2499
2531
  memoryStores = hostMemoryStores();
2500
- strayStores = memoryStores.map((dir2) => ({
2501
- dir: dir2,
2502
- files: existsSync12(dir2) ? readdirSync8(dir2).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort() : []
2503
- })).filter((s) => s.files.length > 0);
2532
+ strayStores = memoryStores.map((dir2) => {
2533
+ let files3 = [];
2534
+ try {
2535
+ if (existsSync12(dir2))
2536
+ files3 = readdirSync8(dir2).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
2537
+ } catch (e) {
2538
+ console.error(`⚠ host auto-memory sweep: could not read ${dir2} (${e instanceof Error ? e.message : String(e)}) - skipping it`);
2539
+ }
2540
+ if (memoryIndexHasContent(dir2))
2541
+ files3.push("MEMORY.md (has content)");
2542
+ return { dir: dir2, files: files3 };
2543
+ }).filter((s) => s.files.length > 0);
2504
2544
  strayCount = strayStores.reduce((n, s) => n + s.files.length, 0);
2505
2545
  for (const s of strayStores)
2506
2546
  review.push(`- [ ] host auto-memory not empty — ${s.files.length} note(s) in ${s.dir}; migrate knowledge → vault notes, behavior → a CLAUDE.local.md fragment, then empty it`);
@@ -2754,7 +2794,7 @@ var init_cli = __esm(async () => {
2754
2794
  const home = requireVaultHome();
2755
2795
  const { backend, skipPermissions, passthrough } = resolveLaunch(rest, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
2756
2796
  const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, home) : passthrough;
2757
- const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
2797
+ const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
2758
2798
  process.exit(launchBackend(backend, home, backend.renderArgs(spec), spec.env));
2759
2799
  }
2760
2800
  case "context": {
@@ -2777,7 +2817,7 @@ var init_cli = __esm(async () => {
2777
2817
  console.log("set: imprnt agent <claude|gemini> override one session: imp --gemini | imp --claude");
2778
2818
  break;
2779
2819
  }
2780
- if (!(name in backends)) {
2820
+ if (!Object.hasOwn(backends, name)) {
2781
2821
  console.error(`unknown agent "${name}" — valid: ${Object.keys(backends).join(", ")}`);
2782
2822
  process.exit(1);
2783
2823
  }
@@ -2811,29 +2851,32 @@ var init_cli = __esm(async () => {
2811
2851
  break;
2812
2852
  }
2813
2853
  case "model": {
2854
+ const envAgent = process.env.IMPRNT_AGENT;
2855
+ const agentName = envAgent ?? readDefaultAgent() ?? "claude";
2856
+ const agent = agentName in backends ? agentName : "claude";
2814
2857
  const [arg] = rest;
2815
2858
  if (!arg) {
2816
- const m = readDefaultModel();
2817
- console.log(`default model: ${m ?? "(agent's own default)"}`);
2859
+ const m = readDefaultModel(agent);
2860
+ console.log(`default model for ${agent}: ${m ?? "(agent's own default)"}`);
2818
2861
  console.log("set: imprnt model <alias|id> clear: imprnt model off");
2819
2862
  console.log(`gemini aliases: ${Object.entries(GEMINI_MODEL_ALIASES).map(([k, v]) => `${k}=${v}`).join(", ")}`);
2820
2863
  break;
2821
2864
  }
2822
2865
  if (arg === "off" || arg === "default" || arg === "none") {
2823
- const r2 = setDefaultModel("");
2866
+ const r2 = setDefaultModel("", agent);
2824
2867
  if (!r2.ok) {
2825
2868
  console.error(`could not clear default model (${configPath()}): ${r2.error}`);
2826
2869
  process.exit(1);
2827
2870
  }
2828
- console.log(`default model cleared (${configPath()}) — the agent uses its own default`);
2871
+ console.log(`default model for ${agent} cleared (${configPath()}) — the agent uses its own default`);
2829
2872
  break;
2830
2873
  }
2831
- const r = setDefaultModel(arg);
2874
+ const r = setDefaultModel(arg, agent);
2832
2875
  if (!r.ok) {
2833
2876
  console.error(`could not set default model (${configPath()}): ${r.error}`);
2834
2877
  process.exit(1);
2835
2878
  }
2836
- console.log(`default model set to ${arg} (${configPath()})`);
2879
+ console.log(`default model for ${agent} set to ${arg} (${configPath()})`);
2837
2880
  break;
2838
2881
  }
2839
2882
  case "plugin": {
@@ -3104,7 +3147,10 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3104
3147
  added.push("CLAUDE.md");
3105
3148
  }
3106
3149
  try {
3150
+ const prevMode = statSync9(target).mode & 511;
3107
3151
  chmodSync(target, 448);
3152
+ if (prevMode !== 448)
3153
+ console.log(`locked ${target} to owner-only (chmod 700, was ${prevMode.toString(8).padStart(3, "0")})`);
3108
3154
  } catch (e) {
3109
3155
  console.error(`could not set owner-only (chmod 700) on ${target}: ${e instanceof Error ? e.message : String(e)} — tighten it by hand to honor the private-vault promise`);
3110
3156
  }
@@ -3154,7 +3200,7 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3154
3200
  const raw = bare ? [] : [cmd, ...rest];
3155
3201
  const { backend, skipPermissions, passthrough } = resolveLaunch(raw, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
3156
3202
  const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, process.cwd()) : passthrough;
3157
- const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
3203
+ const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
3158
3204
  process.exit(launchBackend(backend, process.cwd(), backend.renderArgs(spec), spec.env));
3159
3205
  }
3160
3206
  console.log(`imprnt — deterministic-first markdown knowledge vault
@@ -3174,7 +3220,7 @@ engine (same subcommands under \`imp\` or \`imprnt\`):
3174
3220
  imprnt context print the vault contract — agents run this before writing any note
3175
3221
  imprnt agent [claude|gemini] show or set the per-machine default agent backend for imp
3176
3222
  imprnt yolo [on|off] show or set the per-machine skip-permissions default for imp
3177
- imprnt model [alias|id|off] show or set the per-machine default model (gemini aliases: pro, flash, pro25, lite, ...)
3223
+ imprnt model [alias|id|off] show or set the per-machine default model for the current default agent (gemini aliases: pro, flash, pro25, lite, ...)
3178
3224
  imprnt check [--all] [--vault D] integrity (orphan links, disconnected notes, uncovered snapshots) + regenerate index.md; --all also runs each plugins/*/check.js
3179
3225
  imprnt ingest --apply <file> [--vault D] file a pre-enriched staged note from a plugin into the vault (snapshot + resolve); --apply-all globs plugins/*/proposed/
3180
3226
  imprnt hot [--vault D] needs-review + the session primer
package/dist/imp.js CHANGED
@@ -737,12 +737,19 @@ function readRegistry() {
737
737
  for (const [k, v] of Object.entries(raw?.vaults ?? {}))
738
738
  if (typeof v === "string")
739
739
  vaults[k] = v;
740
+ const defaultModels = {};
741
+ for (const [k, v] of Object.entries(raw?.defaultModels ?? {}))
742
+ if (typeof v === "string")
743
+ defaultModels[k] = v;
744
+ const passthrough = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
740
745
  return {
746
+ ...passthrough,
741
747
  default: typeof raw?.default === "string" ? raw.default : undefined,
742
748
  vaults,
743
749
  defaultAgent: typeof raw?.defaultAgent === "string" ? raw.defaultAgent : undefined,
744
750
  skipPermissions: typeof raw?.skipPermissions === "boolean" ? raw.skipPermissions : undefined,
745
- defaultModel: typeof raw?.defaultModel === "string" ? raw.defaultModel : undefined
751
+ defaultModel: typeof raw?.defaultModel === "string" ? raw.defaultModel : undefined,
752
+ defaultModels: Object.keys(defaultModels).length ? defaultModels : undefined
746
753
  };
747
754
  } catch {
748
755
  return { vaults: {} };
@@ -787,12 +794,25 @@ function setSkipPermissions(on) {
787
794
  reg.skipPermissions = on;
788
795
  return writeRegistry(reg);
789
796
  }
790
- function readDefaultModel() {
791
- return readRegistry().defaultModel;
797
+ function readDefaultModel(agent) {
798
+ const reg = readRegistry();
799
+ const scoped = reg.defaultModels?.[agent];
800
+ if (scoped !== undefined)
801
+ return scoped;
802
+ return agent === (reg.defaultAgent ?? "claude") ? reg.defaultModel : undefined;
792
803
  }
793
- function setDefaultModel(model) {
804
+ function setDefaultModel(model, agent) {
794
805
  const reg = readRegistry();
795
- reg.defaultModel = model || undefined;
806
+ const models = { ...reg.defaultModels ?? {} };
807
+ const legacyAgent = reg.defaultAgent ?? "claude";
808
+ if (reg.defaultModel && models[legacyAgent] === undefined)
809
+ models[legacyAgent] = reg.defaultModel;
810
+ reg.defaultModel = undefined;
811
+ if (model)
812
+ models[agent] = model;
813
+ else
814
+ delete models[agent];
815
+ reg.defaultModels = Object.keys(models).length ? models : undefined;
796
816
  return writeRegistry(reg);
797
817
  }
798
818
  function writeRegistry(reg) {
@@ -1005,14 +1025,19 @@ function joinFragments(parts) {
1005
1025
 
1006
1026
  `);
1007
1027
  }
1028
+ function flagHead(args) {
1029
+ const term = args.indexOf("--");
1030
+ return term < 0 ? args : args.slice(0, term);
1031
+ }
1008
1032
  function claudeRenderArgs(spec) {
1009
1033
  const systemPrompt = spec.insideProject ? spec.globals : joinFragments([spec.cast, spec.pointer, spec.globals]);
1010
1034
  const harness = spec.pluginsRoot ? harnessFlags(spec.pluginsRoot) : [];
1011
1035
  const extra = spec.addDirs.flatMap((d) => ["--add-dir", d]);
1012
1036
  const lead = [];
1013
- if (spec.skipPermissions && !spec.passthrough.includes("--dangerously-skip-permissions"))
1037
+ const head = flagHead(spec.passthrough);
1038
+ if (spec.skipPermissions && !head.includes("--dangerously-skip-permissions"))
1014
1039
  lead.push("--dangerously-skip-permissions");
1015
- if (spec.model && !spec.passthrough.some((a) => a === "--model" || a.startsWith("--model=")))
1040
+ if (spec.model && !head.some((a) => a === "--model" || a.startsWith("--model=")))
1016
1041
  lead.push("--model", spec.model);
1017
1042
  const pass = lead.length ? [...lead, ...spec.passthrough] : spec.passthrough;
1018
1043
  const body = systemPrompt ? mergeFragment(pass, systemPrompt, extra) : [...pass, ...extra];
@@ -1052,15 +1077,15 @@ function geminiRenderArgs(spec) {
1052
1077
  console.error("imp: gemini does not host claude harness plugins (statusline, timemachine) - skipping them");
1053
1078
  }
1054
1079
  const lead = [];
1080
+ const head = flagHead(spec.passthrough);
1055
1081
  if (spec.skipPermissions) {
1056
- if (!spec.passthrough.includes("--yolo") && !spec.passthrough.includes("-y"))
1082
+ if (!head.includes("--yolo") && !head.includes("-y"))
1057
1083
  lead.push("--yolo");
1058
- if (!spec.passthrough.includes("--skip-trust"))
1084
+ if (!head.includes("--skip-trust"))
1059
1085
  lead.push("--skip-trust");
1060
1086
  }
1061
1087
  const pass = [...spec.passthrough];
1062
- const mTerm = pass.indexOf("--");
1063
- const mEnd = mTerm < 0 ? pass.length : mTerm;
1088
+ const mEnd = flagHead(pass).length;
1064
1089
  const isModelFlag = (a) => a === "-m" || a === "--model" || a.startsWith("-m=") || a.startsWith("--model=");
1065
1090
  const mIdx = pass.slice(0, mEnd).findIndex(isModelFlag);
1066
1091
  if (mIdx >= 0) {
@@ -1182,7 +1207,7 @@ function resolveLaunch(passthrough, config = {}) {
1182
1207
  rest.push(a);
1183
1208
  }
1184
1209
  const name = agentFlag ?? process.env.IMPRNT_AGENT ?? config.agent ?? "claude";
1185
- let backend = backends[name];
1210
+ let backend = Object.hasOwn(backends, name) ? backends[name] : undefined;
1186
1211
  if (!backend) {
1187
1212
  console.error(`imp: unknown agent "${name}" - falling back to claude (valid: ${Object.keys(backends).join(", ")})`);
1188
1213
  backend = claudeBackend;
@@ -2318,6 +2343,13 @@ function hostMemoryStores() {
2318
2343
  }
2319
2344
  }).sort();
2320
2345
  }
2346
+ function memoryIndexHasContent(dir2) {
2347
+ try {
2348
+ return readFileSync13(join14(dir2, "MEMORY.md"), "utf8").split(/\r?\n/).some((l) => l.trim() !== "" && l.trim() !== "# Memory index");
2349
+ } catch {
2350
+ return false;
2351
+ }
2352
+ }
2321
2353
  function syncNeedsReview(lines2) {
2322
2354
  const p = join14(vault4, "_needs-review.md");
2323
2355
  const exists = existsSync12(p);
@@ -2497,10 +2529,18 @@ var init_check = __esm(() => {
2497
2529
  for (const r of uncovered)
2498
2530
  review.push(`- [ ] unclassified snapshot \`${r}\` — no vault note points back`);
2499
2531
  memoryStores = hostMemoryStores();
2500
- strayStores = memoryStores.map((dir2) => ({
2501
- dir: dir2,
2502
- files: existsSync12(dir2) ? readdirSync8(dir2).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort() : []
2503
- })).filter((s) => s.files.length > 0);
2532
+ strayStores = memoryStores.map((dir2) => {
2533
+ let files3 = [];
2534
+ try {
2535
+ if (existsSync12(dir2))
2536
+ files3 = readdirSync8(dir2).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
2537
+ } catch (e) {
2538
+ console.error(`⚠ host auto-memory sweep: could not read ${dir2} (${e instanceof Error ? e.message : String(e)}) - skipping it`);
2539
+ }
2540
+ if (memoryIndexHasContent(dir2))
2541
+ files3.push("MEMORY.md (has content)");
2542
+ return { dir: dir2, files: files3 };
2543
+ }).filter((s) => s.files.length > 0);
2504
2544
  strayCount = strayStores.reduce((n, s) => n + s.files.length, 0);
2505
2545
  for (const s of strayStores)
2506
2546
  review.push(`- [ ] host auto-memory not empty — ${s.files.length} note(s) in ${s.dir}; migrate knowledge → vault notes, behavior → a CLAUDE.local.md fragment, then empty it`);
@@ -2754,7 +2794,7 @@ var init_cli = __esm(async () => {
2754
2794
  const home = requireVaultHome();
2755
2795
  const { backend, skipPermissions, passthrough } = resolveLaunch(rest, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
2756
2796
  const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, home) : passthrough;
2757
- const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
2797
+ const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
2758
2798
  process.exit(launchBackend(backend, home, backend.renderArgs(spec), spec.env));
2759
2799
  }
2760
2800
  case "context": {
@@ -2777,7 +2817,7 @@ var init_cli = __esm(async () => {
2777
2817
  console.log("set: imprnt agent <claude|gemini> override one session: imp --gemini | imp --claude");
2778
2818
  break;
2779
2819
  }
2780
- if (!(name in backends)) {
2820
+ if (!Object.hasOwn(backends, name)) {
2781
2821
  console.error(`unknown agent "${name}" — valid: ${Object.keys(backends).join(", ")}`);
2782
2822
  process.exit(1);
2783
2823
  }
@@ -2811,29 +2851,32 @@ var init_cli = __esm(async () => {
2811
2851
  break;
2812
2852
  }
2813
2853
  case "model": {
2854
+ const envAgent = process.env.IMPRNT_AGENT;
2855
+ const agentName = envAgent ?? readDefaultAgent() ?? "claude";
2856
+ const agent = agentName in backends ? agentName : "claude";
2814
2857
  const [arg] = rest;
2815
2858
  if (!arg) {
2816
- const m = readDefaultModel();
2817
- console.log(`default model: ${m ?? "(agent's own default)"}`);
2859
+ const m = readDefaultModel(agent);
2860
+ console.log(`default model for ${agent}: ${m ?? "(agent's own default)"}`);
2818
2861
  console.log("set: imprnt model <alias|id> clear: imprnt model off");
2819
2862
  console.log(`gemini aliases: ${Object.entries(GEMINI_MODEL_ALIASES).map(([k, v]) => `${k}=${v}`).join(", ")}`);
2820
2863
  break;
2821
2864
  }
2822
2865
  if (arg === "off" || arg === "default" || arg === "none") {
2823
- const r2 = setDefaultModel("");
2866
+ const r2 = setDefaultModel("", agent);
2824
2867
  if (!r2.ok) {
2825
2868
  console.error(`could not clear default model (${configPath()}): ${r2.error}`);
2826
2869
  process.exit(1);
2827
2870
  }
2828
- console.log(`default model cleared (${configPath()}) — the agent uses its own default`);
2871
+ console.log(`default model for ${agent} cleared (${configPath()}) — the agent uses its own default`);
2829
2872
  break;
2830
2873
  }
2831
- const r = setDefaultModel(arg);
2874
+ const r = setDefaultModel(arg, agent);
2832
2875
  if (!r.ok) {
2833
2876
  console.error(`could not set default model (${configPath()}): ${r.error}`);
2834
2877
  process.exit(1);
2835
2878
  }
2836
- console.log(`default model set to ${arg} (${configPath()})`);
2879
+ console.log(`default model for ${agent} set to ${arg} (${configPath()})`);
2837
2880
  break;
2838
2881
  }
2839
2882
  case "plugin": {
@@ -3104,7 +3147,10 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3104
3147
  added.push("CLAUDE.md");
3105
3148
  }
3106
3149
  try {
3150
+ const prevMode = statSync9(target).mode & 511;
3107
3151
  chmodSync(target, 448);
3152
+ if (prevMode !== 448)
3153
+ console.log(`locked ${target} to owner-only (chmod 700, was ${prevMode.toString(8).padStart(3, "0")})`);
3108
3154
  } catch (e) {
3109
3155
  console.error(`could not set owner-only (chmod 700) on ${target}: ${e instanceof Error ? e.message : String(e)} — tighten it by hand to honor the private-vault promise`);
3110
3156
  }
@@ -3154,7 +3200,7 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3154
3200
  const raw = bare ? [] : [cmd, ...rest];
3155
3201
  const { backend, skipPermissions, passthrough } = resolveLaunch(raw, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
3156
3202
  const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, process.cwd()) : passthrough;
3157
- const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
3203
+ const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
3158
3204
  process.exit(launchBackend(backend, process.cwd(), backend.renderArgs(spec), spec.env));
3159
3205
  }
3160
3206
  console.log(`imprnt — deterministic-first markdown knowledge vault
@@ -3174,7 +3220,7 @@ engine (same subcommands under \`imp\` or \`imprnt\`):
3174
3220
  imprnt context print the vault contract — agents run this before writing any note
3175
3221
  imprnt agent [claude|gemini] show or set the per-machine default agent backend for imp
3176
3222
  imprnt yolo [on|off] show or set the per-machine skip-permissions default for imp
3177
- imprnt model [alias|id|off] show or set the per-machine default model (gemini aliases: pro, flash, pro25, lite, ...)
3223
+ imprnt model [alias|id|off] show or set the per-machine default model for the current default agent (gemini aliases: pro, flash, pro25, lite, ...)
3178
3224
  imprnt check [--all] [--vault D] integrity (orphan links, disconnected notes, uncovered snapshots) + regenerate index.md; --all also runs each plugins/*/check.js
3179
3225
  imprnt ingest --apply <file> [--vault D] file a pre-enriched staged note from a plugin into the vault (snapshot + resolve); --apply-all globs plugins/*/proposed/
3180
3226
  imprnt hot [--vault D] needs-review + the session primer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imprnt",
3
- "version": "0.1.3-edge.61",
3
+ "version": "0.1.3-edge.62",
4
4
  "description": "Deterministic-first, plain-markdown knowledge vault. Code does the bulk transform. The LLM only touches the irreducibly-semantic 20%. Native grep plus BM25 retrieval, zero MCP on the vault.",
5
5
  "type": "module",
6
6
  "bin": {