imprnt 0.1.3-edge.60 → 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: {} };
@@ -766,14 +773,9 @@ function registerVault(path, opts = {}) {
766
773
  const name = opts.name ?? "personal";
767
774
  reg.vaults[name] = path;
768
775
  reg.default = name;
769
- const p = configPath();
770
- try {
771
- mkdirSync2(dirname2(p), { recursive: true });
772
- writeFileSync5(p, JSON.stringify(reg, null, 2) + `
773
- `);
774
- } catch (e) {
775
- return { status: "error", current: path, error: e instanceof Error ? e.message : String(e) };
776
- }
776
+ const w = writeRegistry(reg);
777
+ if (!w.ok)
778
+ return { status: "error", current: path, error: w.error };
777
779
  return { status: "registered", current: path };
778
780
  }
779
781
  function readDefaultAgent() {
@@ -792,12 +794,25 @@ function setSkipPermissions(on) {
792
794
  reg.skipPermissions = on;
793
795
  return writeRegistry(reg);
794
796
  }
795
- function readDefaultModel() {
796
- 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;
797
803
  }
798
- function setDefaultModel(model) {
804
+ function setDefaultModel(model, agent) {
799
805
  const reg = readRegistry();
800
- 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;
801
816
  return writeRegistry(reg);
802
817
  }
803
818
  function writeRegistry(reg) {
@@ -842,8 +857,9 @@ var init_registry = () => {};
842
857
  // scripts/lib/launch.ts
843
858
  import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, realpathSync, rmSync as rmSync3, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
844
859
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
845
- import { isAbsolute, join as join8, resolve as resolve4, sep as sep2 } from "node:path";
860
+ import { basename as basename2, isAbsolute, join as join8, resolve as resolve4, sep as sep2 } from "node:path";
846
861
  import { spawnSync as spawnSync2 } from "node:child_process";
862
+ import { createInterface } from "node:readline/promises";
847
863
  function importPath(root, target) {
848
864
  if (target.startsWith("~/"))
849
865
  return join8(homedir2(), target.slice(2));
@@ -1009,11 +1025,21 @@ function joinFragments(parts) {
1009
1025
 
1010
1026
  `);
1011
1027
  }
1028
+ function flagHead(args) {
1029
+ const term = args.indexOf("--");
1030
+ return term < 0 ? args : args.slice(0, term);
1031
+ }
1012
1032
  function claudeRenderArgs(spec) {
1013
1033
  const systemPrompt = spec.insideProject ? spec.globals : joinFragments([spec.cast, spec.pointer, spec.globals]);
1014
1034
  const harness = spec.pluginsRoot ? harnessFlags(spec.pluginsRoot) : [];
1015
1035
  const extra = spec.addDirs.flatMap((d) => ["--add-dir", d]);
1016
- const pass = spec.skipPermissions && !spec.passthrough.includes("--dangerously-skip-permissions") ? ["--dangerously-skip-permissions", ...spec.passthrough] : spec.passthrough;
1036
+ const lead = [];
1037
+ const head = flagHead(spec.passthrough);
1038
+ if (spec.skipPermissions && !head.includes("--dangerously-skip-permissions"))
1039
+ lead.push("--dangerously-skip-permissions");
1040
+ if (spec.model && !head.some((a) => a === "--model" || a.startsWith("--model=")))
1041
+ lead.push("--model", spec.model);
1042
+ const pass = lead.length ? [...lead, ...spec.passthrough] : spec.passthrough;
1017
1043
  const body = systemPrompt ? mergeFragment(pass, systemPrompt, extra) : [...pass, ...extra];
1018
1044
  return [...harness, ...body];
1019
1045
  }
@@ -1023,6 +1049,19 @@ function expandGeminiModel(m) {
1023
1049
  function escapeGeminiImports(text) {
1024
1050
  return text.replaceAll("@", "\\@");
1025
1051
  }
1052
+ function trackGeminiTempDir(dir) {
1053
+ geminiTempDirs.add(dir);
1054
+ if (geminiCleanupArmed)
1055
+ return;
1056
+ geminiCleanupArmed = true;
1057
+ process.on("exit", () => {
1058
+ for (const d of geminiTempDirs) {
1059
+ try {
1060
+ rmSync3(d, { recursive: true, force: true });
1061
+ } catch {}
1062
+ }
1063
+ });
1064
+ }
1026
1065
  function geminiRenderArgs(spec) {
1027
1066
  const includes = [];
1028
1067
  const systemPrompt = joinFragments([spec.cast, spec.pointer, spec.globals]);
@@ -1030,11 +1069,7 @@ function geminiRenderArgs(spec) {
1030
1069
  const dir = mkdtempSync2(join8(tmpdir2(), "imprnt-gemini-"));
1031
1070
  writeFileSync6(join8(dir, "GEMINI.md"), escapeGeminiImports(systemPrompt) + `
1032
1071
  `);
1033
- process.on("exit", () => {
1034
- try {
1035
- rmSync3(dir, { recursive: true, force: true });
1036
- } catch {}
1037
- });
1072
+ trackGeminiTempDir(dir);
1038
1073
  includes.push(dir);
1039
1074
  }
1040
1075
  includes.push(...spec.addDirs);
@@ -1042,23 +1077,30 @@ function geminiRenderArgs(spec) {
1042
1077
  console.error("imp: gemini does not host claude harness plugins (statusline, timemachine) - skipping them");
1043
1078
  }
1044
1079
  const lead = [];
1080
+ const head = flagHead(spec.passthrough);
1045
1081
  if (spec.skipPermissions) {
1046
- if (!spec.passthrough.includes("--yolo") && !spec.passthrough.includes("-y"))
1082
+ if (!head.includes("--yolo") && !head.includes("-y"))
1047
1083
  lead.push("--yolo");
1048
- if (!spec.passthrough.includes("--skip-trust"))
1084
+ if (!head.includes("--skip-trust"))
1049
1085
  lead.push("--skip-trust");
1050
1086
  }
1051
1087
  const pass = [...spec.passthrough];
1052
- const mIdx = pass.findIndex((a) => a === "-m" || a === "--model");
1053
- if (mIdx >= 0 && pass[mIdx + 1] !== undefined) {
1054
- pass[mIdx + 1] = expandGeminiModel(pass[mIdx + 1]);
1055
- } else if (mIdx < 0 && spec.model) {
1088
+ const mEnd = flagHead(pass).length;
1089
+ const isModelFlag = (a) => a === "-m" || a === "--model" || a.startsWith("-m=") || a.startsWith("--model=");
1090
+ const mIdx = pass.slice(0, mEnd).findIndex(isModelFlag);
1091
+ if (mIdx >= 0) {
1092
+ const tok = pass[mIdx];
1093
+ const eq = tok.indexOf("=");
1094
+ if (eq >= 0)
1095
+ pass[mIdx] = tok.slice(0, eq + 1) + expandGeminiModel(tok.slice(eq + 1));
1096
+ else if (mIdx + 1 < mEnd && pass[mIdx + 1] !== undefined)
1097
+ pass[mIdx + 1] = expandGeminiModel(pass[mIdx + 1]);
1098
+ } else if (spec.model) {
1056
1099
  pass.unshift("-m", expandGeminiModel(spec.model));
1057
1100
  }
1058
- const rIdx = pass.findIndex((a) => a === "-r" || a === "--resume");
1059
- if (rIdx >= 0 && (pass[rIdx + 1] === undefined || pass[rIdx + 1].startsWith("-"))) {
1101
+ const rIdx = valuelessResumeIndex(pass);
1102
+ if (rIdx >= 0)
1060
1103
  pass.splice(rIdx + 1, 0, "latest");
1061
- }
1062
1104
  const flags = includes.length ? ["--include-directories", includes.join(",")] : [];
1063
1105
  return [...lead, ...flags, ...pass];
1064
1106
  }
@@ -1071,11 +1113,88 @@ function parseGeminiSessions(stdout) {
1071
1113
  }
1072
1114
  return out;
1073
1115
  }
1116
+ function valuelessResumeIndex(args) {
1117
+ const term = args.indexOf("--");
1118
+ const end = term < 0 ? args.length : term;
1119
+ let i = -1;
1120
+ for (let k = 0;k < end; k++) {
1121
+ if (args[k] === "-r" || args[k] === "--resume") {
1122
+ i = k;
1123
+ break;
1124
+ }
1125
+ }
1126
+ if (i < 0)
1127
+ return -1;
1128
+ const next = i + 1 < end ? args[i + 1] : undefined;
1129
+ return next === undefined || next.startsWith("-") ? i : -1;
1130
+ }
1131
+ function listGeminiSessions(cwd) {
1132
+ const r = spawnSync2("gemini", ["--list-sessions", "--skip-trust"], { cwd, encoding: "utf8" });
1133
+ if (r.error || r.status !== 0)
1134
+ return { sessions: [], ran: false };
1135
+ const stdout = r.stdout || "";
1136
+ const sessions = parseGeminiSessions(stdout);
1137
+ if (!sessions.length && stdout.trim() && !/\(\s*0\s*\)|no sessions/i.test(stdout)) {
1138
+ console.error("imp: couldn't parse gemini's session list (its format may have changed) - starting fresh");
1139
+ }
1140
+ return { sessions, ran: true };
1141
+ }
1142
+ async function pickGeminiSession(sessions, label) {
1143
+ const cols = process.stdout.columns || 80;
1144
+ const nameW = Math.max(20, Math.min(64, cols - 16));
1145
+ const trunc = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "…" : s;
1146
+ process.stderr.write(`
1147
+ resume a chat - gemini · ${label}
1148
+
1149
+ `);
1150
+ sessions.forEach((s, i) => {
1151
+ process.stderr.write(` ${String(i + 1).padStart(2)} ${trunc(s.name).padEnd(nameW)} ${s.age.replace(/\s*ago$/, "")}
1152
+ `);
1153
+ });
1154
+ process.stderr.write(`
1155
+ `);
1156
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1157
+ const answer = (await rl.question(" number to resume · enter for newest · q to cancel › ")).trim();
1158
+ rl.close();
1159
+ if (answer === "")
1160
+ return "latest";
1161
+ if (answer.toLowerCase() === "q")
1162
+ return null;
1163
+ const n = Number(answer);
1164
+ if (Number.isInteger(n) && n >= 1 && n <= sessions.length)
1165
+ return sessions[n - 1].id;
1166
+ process.stderr.write(` not a valid choice - starting a fresh session
1167
+ `);
1168
+ return null;
1169
+ }
1170
+ async function geminiResolveResume(passthrough, cwd) {
1171
+ const rIdx = valuelessResumeIndex(passthrough);
1172
+ if (rIdx < 0)
1173
+ return passthrough;
1174
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
1175
+ return passthrough;
1176
+ const { sessions, ran } = listGeminiSessions(cwd);
1177
+ if (!ran)
1178
+ return passthrough;
1179
+ if (!sessions.length) {
1180
+ console.error("imp: no saved gemini chats for this project yet - starting fresh");
1181
+ return passthrough.filter((_, i) => i !== rIdx);
1182
+ }
1183
+ const chosen = await pickGeminiSession(sessions, basename2(cwd));
1184
+ if (chosen === null)
1185
+ return passthrough.filter((_, i) => i !== rIdx);
1186
+ const out = [...passthrough];
1187
+ out.splice(rIdx + 1, 0, chosen);
1188
+ return out;
1189
+ }
1074
1190
  function resolveLaunch(passthrough, config = {}) {
1191
+ const term = passthrough.indexOf("--");
1192
+ const head = term < 0 ? passthrough : passthrough.slice(0, term);
1193
+ const tail = term < 0 ? [] : passthrough.slice(term);
1075
1194
  let agentFlag;
1076
1195
  let yoloFlag;
1077
1196
  const rest = [];
1078
- for (const a of passthrough) {
1197
+ for (const a of head) {
1079
1198
  if (a === "--gemini")
1080
1199
  agentFlag = agentFlag ?? "gemini";
1081
1200
  else if (a === "--claude")
@@ -1088,14 +1207,15 @@ function resolveLaunch(passthrough, config = {}) {
1088
1207
  rest.push(a);
1089
1208
  }
1090
1209
  const name = agentFlag ?? process.env.IMPRNT_AGENT ?? config.agent ?? "claude";
1091
- let backend = backends[name];
1210
+ let backend = Object.hasOwn(backends, name) ? backends[name] : undefined;
1092
1211
  if (!backend) {
1093
1212
  console.error(`imp: unknown agent "${name}" - falling back to claude (valid: ${Object.keys(backends).join(", ")})`);
1094
1213
  backend = claudeBackend;
1095
1214
  }
1096
- const envYolo = "IMPRNT_YOLO" in process.env ? !["0", "false", ""].includes(process.env.IMPRNT_YOLO ?? "") : undefined;
1215
+ const yoloEnv = process.env.IMPRNT_YOLO;
1216
+ const envYolo = yoloEnv === undefined ? undefined : !["0", "false", "", "off", "no"].includes(yoloEnv.toLowerCase());
1097
1217
  const skipPermissions = yoloFlag ?? envYolo ?? config.yolo ?? false;
1098
- return { backend, skipPermissions, passthrough: rest };
1218
+ return { backend, skipPermissions, passthrough: [...rest, ...tail] };
1099
1219
  }
1100
1220
  function launchBackend(backend, cwd, args, env = process.env) {
1101
1221
  if (!isDir(cwd)) {
@@ -1110,7 +1230,7 @@ function launchBackend(backend, cwd, args, env = process.env) {
1110
1230
  }
1111
1231
  return r.status ?? 1;
1112
1232
  }
1113
- var claudeBackend, GEMINI_MODEL_ALIASES, geminiBackend, backends;
1233
+ var claudeBackend, GEMINI_MODEL_ALIASES, geminiTempDirs, geminiCleanupArmed = false, geminiBackend, backends;
1114
1234
  var init_launch = __esm(() => {
1115
1235
  init_plugins();
1116
1236
  init_global();
@@ -1127,10 +1247,12 @@ var init_launch = __esm(() => {
1127
1247
  gemma31: "gemma-4-31b-it",
1128
1248
  gemma26: "gemma-4-26b-a4b-it"
1129
1249
  };
1250
+ geminiTempDirs = new Set;
1130
1251
  geminiBackend = {
1131
1252
  name: "gemini",
1132
1253
  missingHint: "Install the Gemini CLI first: npm i -g @google/gemini-cli",
1133
- renderArgs: geminiRenderArgs
1254
+ renderArgs: geminiRenderArgs,
1255
+ resolveResume: geminiResolveResume
1134
1256
  };
1135
1257
  backends = {
1136
1258
  claude: claudeBackend,
@@ -1173,7 +1295,7 @@ var init_manifest = () => {};
1173
1295
  var exports_ingest = {};
1174
1296
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync3, existsSync as existsSync9, copyFileSync, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync6 } from "node:fs";
1175
1297
  import { createHash } from "node:crypto";
1176
- import { basename as basename2, extname, join as join10, relative as relative3 } from "node:path";
1298
+ import { basename as basename3, extname, join as join10, relative as relative3 } from "node:path";
1177
1299
  function slugify(s) {
1178
1300
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
1179
1301
  }
@@ -1254,7 +1376,7 @@ function applyStaged(staged, vault) {
1254
1376
  return "error";
1255
1377
  }
1256
1378
  const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? "";
1257
- const slug = deriveSlug([fmScalar2(fm, "slug"), title, basename2(staged, ".md")]);
1379
+ const slug = deriveSlug([fmScalar2(fm, "slug"), title, basename3(staged, ".md")]);
1258
1380
  if (!slug) {
1259
1381
  console.error(` ✗ ${staged}: can't derive a slug - slug:, the H1 title, and the filename all slugify to nothing`);
1260
1382
  return "error";
@@ -1481,7 +1603,7 @@ ${filed} filed, ${noop} no-op, ${conflict} conflict, ${error} error.`);
1481
1603
  console.log(`unchanged (hash ${hash}) — skipping ${src}. note: ${prior.note}`);
1482
1604
  process.exit(0);
1483
1605
  }
1484
- fname = basename2(src);
1606
+ fname = basename3(src);
1485
1607
  dateMatch = fname.match(/(\d{4}-\d{2}-\d{2})/) || text.match(/^\s*date:\s*(\d{4}-\d{2}-\d{2})/im);
1486
1608
  date = dateMatch ? dateMatch[1] : new Date().toISOString().slice(0, 10);
1487
1609
  lines = text.split(/\r?\n/);
@@ -1515,7 +1637,7 @@ ${filed} filed, ${noop} no-op, ${conflict} conflict, ${error} error.`);
1515
1637
  }
1516
1638
  recurringSpeaker = [...turnsBySpeaker.values()].some((n) => n >= 2);
1517
1639
  isTranscript = isFile && looksLikeTranscript(speakers, turnCount, contentLines, recurringSpeaker);
1518
- slugBasis = slugHint || subject || [...speakers].join("-") || (isFile ? basename2(src, extname(src)) : text.slice(0, 60));
1640
+ slugBasis = slugHint || subject || [...speakers].join("-") || (isFile ? basename3(src, extname(src)) : text.slice(0, 60));
1519
1641
  subjectSlug = slugify(slugBasis) || "source";
1520
1642
  noteSlug = `${date}-${subjectSlug}`;
1521
1643
  rawDir = join10(vault, "..", "raw", isTranscript ? "transcripts" : "adhoc");
@@ -1756,7 +1878,7 @@ var init_tags = __esm(() => {
1756
1878
  // scripts/recall.ts
1757
1879
  var exports_recall = {};
1758
1880
  import { lstatSync as lstatSync2, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "node:fs";
1759
- import { basename as basename3, join as join12, relative as relative4 } from "node:path";
1881
+ import { basename as basename4, join as join12, relative as relative4 } from "node:path";
1760
1882
  function walk2(dir2) {
1761
1883
  const out = [];
1762
1884
  for (const entry of readdirSync6(dir2)) {
@@ -1973,7 +2095,7 @@ var init_recall = __esm(() => {
1973
2095
  for (const t of tokens)
1974
2096
  tf.set(t, (tf.get(t) ?? 0) + weight);
1975
2097
  };
1976
- add([...tokenize(titleText), ...tokenize(basename3(path, ".md"))], TITLE_BOOST);
2098
+ add([...tokenize(titleText), ...tokenize(basename4(path, ".md"))], TITLE_BOOST);
1977
2099
  add(tokenize(aliases), TITLE_BOOST);
1978
2100
  add(tags.flatMap(tokenize), TAG_BOOST);
1979
2101
  add(tokenize(summary), SUMMARY_BOOST);
@@ -2038,7 +2160,7 @@ var init_recall = __esm(() => {
2038
2160
  var exports_snapshot = {};
2039
2161
  import { readdirSync as readdirSync7, readFileSync as readFileSync12, copyFileSync as copyFileSync2, mkdirSync as mkdirSync4, existsSync as existsSync11, statSync as statSync7, lstatSync as lstatSync3 } from "node:fs";
2040
2162
  import { createHash as createHash2 } from "node:crypto";
2041
- import { join as join13, relative as relative5, basename as basename4, extname as extname2, resolve as resolve5, sep as sep3 } from "node:path";
2163
+ import { join as join13, relative as relative5, basename as basename5, extname as extname2, resolve as resolve5, sep as sep3 } from "node:path";
2042
2164
  function destWithinRaw(rawRoot, dest) {
2043
2165
  const rootResolved = resolve5(rawRoot);
2044
2166
  const destResolved = resolve5(rootResolved, dest);
@@ -2050,7 +2172,7 @@ function collect(p, base) {
2050
2172
  let skippedLinks = 0;
2051
2173
  const st = statSync7(p);
2052
2174
  if (st.isFile())
2053
- return { files: [{ abs: p, rel: basename4(p) }], skippedLinks };
2175
+ return { files: [{ abs: p, rel: basename5(p) }], skippedLinks };
2054
2176
  const out = [];
2055
2177
  const walk3 = (dir2) => {
2056
2178
  for (const entry of readdirSync7(dir2)) {
@@ -2221,6 +2343,13 @@ function hostMemoryStores() {
2221
2343
  }
2222
2344
  }).sort();
2223
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
+ }
2224
2353
  function syncNeedsReview(lines2) {
2225
2354
  const p = join14(vault4, "_needs-review.md");
2226
2355
  const exists = existsSync12(p);
@@ -2400,10 +2529,18 @@ var init_check = __esm(() => {
2400
2529
  for (const r of uncovered)
2401
2530
  review.push(`- [ ] unclassified snapshot \`${r}\` — no vault note points back`);
2402
2531
  memoryStores = hostMemoryStores();
2403
- strayStores = memoryStores.map((dir2) => ({
2404
- dir: dir2,
2405
- files: existsSync12(dir2) ? readdirSync8(dir2).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort() : []
2406
- })).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);
2407
2544
  strayCount = strayStores.reduce((n, s) => n + s.files.length, 0);
2408
2545
  for (const s of strayStores)
2409
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`);
@@ -2528,10 +2665,10 @@ all plugin checks passed.`);
2528
2665
  var exports_cli = {};
2529
2666
  import { cpSync as cpSync3, mkdirSync as mkdirSync5, chmodSync, existsSync as existsSync13, readFileSync as readFileSync14, lstatSync as lstatSync4, statSync as statSync9, realpathSync as realpathSync2 } from "node:fs";
2530
2667
  import { spawnSync as spawnSync4 } from "node:child_process";
2531
- import { join as join15, dirname as dirname4, resolve as resolve6, basename as basename5 } from "node:path";
2668
+ import { join as join15, dirname as dirname4, resolve as resolve6, basename as basename6 } from "node:path";
2532
2669
  import { homedir as homedir4 } from "node:os";
2533
2670
  import { fileURLToPath } from "node:url";
2534
- import { createInterface } from "node:readline/promises";
2671
+ import { createInterface as createInterface2 } from "node:readline/promises";
2535
2672
  function pluginRoot() {
2536
2673
  const local = projectRoot();
2537
2674
  if (process.env.IMPRNT_ROOT || process.env.IMPRINT_ROOT)
@@ -2578,7 +2715,7 @@ function physicalTarget(target) {
2578
2715
  const parent = dirname4(existing);
2579
2716
  if (parent === existing)
2580
2717
  return target;
2581
- tail.unshift(basename5(existing));
2718
+ tail.unshift(basename6(existing));
2582
2719
  existing = parent;
2583
2720
  }
2584
2721
  try {
@@ -2607,61 +2744,6 @@ function controlSlot(dst) {
2607
2744
  return "keep";
2608
2745
  return "blocked-non-regular-file";
2609
2746
  }
2610
- function listGeminiSessions(cwd) {
2611
- const r = spawnSync4("gemini", ["--list-sessions", "--skip-trust"], { cwd, encoding: "utf8" });
2612
- return parseGeminiSessions(r.stdout || "");
2613
- }
2614
- async function pickGeminiSession(sessions, label) {
2615
- const cols = process.stdout.columns || 80;
2616
- const nameW = Math.max(20, Math.min(64, cols - 16));
2617
- const trunc = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "…" : s;
2618
- process.stderr.write(`
2619
- resume a chat — gemini · ${label}
2620
-
2621
- `);
2622
- sessions.forEach((s, i) => {
2623
- process.stderr.write(` ${String(i + 1).padStart(2)} ${trunc(s.name).padEnd(nameW)} ${s.age.replace(/\s*ago$/, "")}
2624
- `);
2625
- });
2626
- process.stderr.write(`
2627
- `);
2628
- const rl = createInterface({ input: process.stdin, output: process.stderr });
2629
- const answer = (await rl.question(" number to resume · enter for newest · q to cancel › ")).trim();
2630
- rl.close();
2631
- if (answer === "")
2632
- return "latest";
2633
- if (answer.toLowerCase() === "q")
2634
- return null;
2635
- const n = Number(answer);
2636
- if (Number.isInteger(n) && n >= 1 && n <= sessions.length)
2637
- return sessions[n - 1].id;
2638
- process.stderr.write(` not a valid choice — starting a fresh session
2639
- `);
2640
- return null;
2641
- }
2642
- async function resolveGeminiResume(backendName, passthrough, cwd) {
2643
- if (backendName !== "gemini")
2644
- return passthrough;
2645
- const rIdx = passthrough.findIndex((a) => a === "-r" || a === "--resume");
2646
- if (rIdx < 0)
2647
- return passthrough;
2648
- const next = passthrough[rIdx + 1];
2649
- if (next !== undefined && !next.startsWith("-"))
2650
- return passthrough;
2651
- if (!(process.stdin.isTTY && process.stdout.isTTY))
2652
- return passthrough;
2653
- const sessions = listGeminiSessions(cwd);
2654
- if (!sessions.length) {
2655
- console.error("imp: no saved gemini chats for this project yet — starting fresh");
2656
- return passthrough.filter((_, i) => i !== rIdx);
2657
- }
2658
- const chosen = await pickGeminiSession(sessions, basename5(cwd));
2659
- if (chosen === null)
2660
- return passthrough.filter((_, i) => i !== rIdx);
2661
- const out = [...passthrough];
2662
- out.splice(rIdx + 1, 0, chosen);
2663
- return out;
2664
- }
2665
2747
  var here, pkgRoot, cmd, rest, asImp;
2666
2748
  var init_cli = __esm(async () => {
2667
2749
  init_resolve();
@@ -2711,8 +2793,8 @@ var init_cli = __esm(async () => {
2711
2793
  case "lair": {
2712
2794
  const home = requireVaultHome();
2713
2795
  const { backend, skipPermissions, passthrough } = resolveLaunch(rest, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
2714
- const resumed = await resolveGeminiResume(backend.name, passthrough, home);
2715
- const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
2796
+ const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, home) : passthrough;
2797
+ const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
2716
2798
  process.exit(launchBackend(backend, home, backend.renderArgs(spec), spec.env));
2717
2799
  }
2718
2800
  case "context": {
@@ -2735,8 +2817,8 @@ var init_cli = __esm(async () => {
2735
2817
  console.log("set: imprnt agent <claude|gemini> override one session: imp --gemini | imp --claude");
2736
2818
  break;
2737
2819
  }
2738
- if (name !== "claude" && name !== "gemini") {
2739
- console.error(`unknown agent "${name}" — valid: claude, gemini`);
2820
+ if (!Object.hasOwn(backends, name)) {
2821
+ console.error(`unknown agent "${name}" — valid: ${Object.keys(backends).join(", ")}`);
2740
2822
  process.exit(1);
2741
2823
  }
2742
2824
  const r = setDefaultAgent(name);
@@ -2769,29 +2851,32 @@ var init_cli = __esm(async () => {
2769
2851
  break;
2770
2852
  }
2771
2853
  case "model": {
2854
+ const envAgent = process.env.IMPRNT_AGENT;
2855
+ const agentName = envAgent ?? readDefaultAgent() ?? "claude";
2856
+ const agent = agentName in backends ? agentName : "claude";
2772
2857
  const [arg] = rest;
2773
2858
  if (!arg) {
2774
- const m = readDefaultModel();
2775
- 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)"}`);
2776
2861
  console.log("set: imprnt model <alias|id> clear: imprnt model off");
2777
- console.log("gemini aliases: pro=gemini-3.1-pro-preview, flash=gemini-3.5-flash, pro25=gemini-2.5-pro, lite=gemini-3.1-flash-lite, gemma31, gemma26");
2862
+ console.log(`gemini aliases: ${Object.entries(GEMINI_MODEL_ALIASES).map(([k, v]) => `${k}=${v}`).join(", ")}`);
2778
2863
  break;
2779
2864
  }
2780
2865
  if (arg === "off" || arg === "default" || arg === "none") {
2781
- const r2 = setDefaultModel("");
2866
+ const r2 = setDefaultModel("", agent);
2782
2867
  if (!r2.ok) {
2783
2868
  console.error(`could not clear default model (${configPath()}): ${r2.error}`);
2784
2869
  process.exit(1);
2785
2870
  }
2786
- 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`);
2787
2872
  break;
2788
2873
  }
2789
- const r = setDefaultModel(arg);
2874
+ const r = setDefaultModel(arg, agent);
2790
2875
  if (!r.ok) {
2791
2876
  console.error(`could not set default model (${configPath()}): ${r.error}`);
2792
2877
  process.exit(1);
2793
2878
  }
2794
- console.log(`default model set to ${arg} (${configPath()})`);
2879
+ console.log(`default model for ${agent} set to ${arg} (${configPath()})`);
2795
2880
  break;
2796
2881
  }
2797
2882
  case "plugin": {
@@ -2999,7 +3084,7 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
2999
3084
  const inPlace = isVaultProject(process.cwd());
3000
3085
  const display = inPlace ? process.cwd() : "~/imprnt";
3001
3086
  const fallback = inPlace ? process.cwd() : join15(homedir4(), "imprnt");
3002
- const rl = createInterface({ input: process.stdin, output: process.stdout });
3087
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3003
3088
  const answer = (await rl.question(`vault location [${display}]: `)).trim();
3004
3089
  rl.close();
3005
3090
  target = answer ? resolvePath(answer) : fallback;
@@ -3062,7 +3147,10 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3062
3147
  added.push("CLAUDE.md");
3063
3148
  }
3064
3149
  try {
3150
+ const prevMode = statSync9(target).mode & 511;
3065
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")})`);
3066
3154
  } catch (e) {
3067
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`);
3068
3156
  }
@@ -3111,8 +3199,8 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3111
3199
  }
3112
3200
  const raw = bare ? [] : [cmd, ...rest];
3113
3201
  const { backend, skipPermissions, passthrough } = resolveLaunch(raw, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
3114
- const resumed = await resolveGeminiResume(backend.name, passthrough, process.cwd());
3115
- const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
3202
+ const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, process.cwd()) : passthrough;
3203
+ const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
3116
3204
  process.exit(launchBackend(backend, process.cwd(), backend.renderArgs(spec), spec.env));
3117
3205
  }
3118
3206
  console.log(`imprnt — deterministic-first markdown knowledge vault
@@ -3132,7 +3220,7 @@ engine (same subcommands under \`imp\` or \`imprnt\`):
3132
3220
  imprnt context print the vault contract — agents run this before writing any note
3133
3221
  imprnt agent [claude|gemini] show or set the per-machine default agent backend for imp
3134
3222
  imprnt yolo [on|off] show or set the per-machine skip-permissions default for imp
3135
- 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, ...)
3136
3224
  imprnt check [--all] [--vault D] integrity (orphan links, disconnected notes, uncovered snapshots) + regenerate index.md; --all also runs each plugins/*/check.js
3137
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/
3138
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: {} };
@@ -766,14 +773,9 @@ function registerVault(path, opts = {}) {
766
773
  const name = opts.name ?? "personal";
767
774
  reg.vaults[name] = path;
768
775
  reg.default = name;
769
- const p = configPath();
770
- try {
771
- mkdirSync2(dirname2(p), { recursive: true });
772
- writeFileSync5(p, JSON.stringify(reg, null, 2) + `
773
- `);
774
- } catch (e) {
775
- return { status: "error", current: path, error: e instanceof Error ? e.message : String(e) };
776
- }
776
+ const w = writeRegistry(reg);
777
+ if (!w.ok)
778
+ return { status: "error", current: path, error: w.error };
777
779
  return { status: "registered", current: path };
778
780
  }
779
781
  function readDefaultAgent() {
@@ -792,12 +794,25 @@ function setSkipPermissions(on) {
792
794
  reg.skipPermissions = on;
793
795
  return writeRegistry(reg);
794
796
  }
795
- function readDefaultModel() {
796
- 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;
797
803
  }
798
- function setDefaultModel(model) {
804
+ function setDefaultModel(model, agent) {
799
805
  const reg = readRegistry();
800
- 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;
801
816
  return writeRegistry(reg);
802
817
  }
803
818
  function writeRegistry(reg) {
@@ -842,8 +857,9 @@ var init_registry = () => {};
842
857
  // scripts/lib/launch.ts
843
858
  import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, realpathSync, rmSync as rmSync3, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
844
859
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
845
- import { isAbsolute, join as join8, resolve as resolve4, sep as sep2 } from "node:path";
860
+ import { basename as basename2, isAbsolute, join as join8, resolve as resolve4, sep as sep2 } from "node:path";
846
861
  import { spawnSync as spawnSync2 } from "node:child_process";
862
+ import { createInterface } from "node:readline/promises";
847
863
  function importPath(root, target) {
848
864
  if (target.startsWith("~/"))
849
865
  return join8(homedir2(), target.slice(2));
@@ -1009,11 +1025,21 @@ function joinFragments(parts) {
1009
1025
 
1010
1026
  `);
1011
1027
  }
1028
+ function flagHead(args) {
1029
+ const term = args.indexOf("--");
1030
+ return term < 0 ? args : args.slice(0, term);
1031
+ }
1012
1032
  function claudeRenderArgs(spec) {
1013
1033
  const systemPrompt = spec.insideProject ? spec.globals : joinFragments([spec.cast, spec.pointer, spec.globals]);
1014
1034
  const harness = spec.pluginsRoot ? harnessFlags(spec.pluginsRoot) : [];
1015
1035
  const extra = spec.addDirs.flatMap((d) => ["--add-dir", d]);
1016
- const pass = spec.skipPermissions && !spec.passthrough.includes("--dangerously-skip-permissions") ? ["--dangerously-skip-permissions", ...spec.passthrough] : spec.passthrough;
1036
+ const lead = [];
1037
+ const head = flagHead(spec.passthrough);
1038
+ if (spec.skipPermissions && !head.includes("--dangerously-skip-permissions"))
1039
+ lead.push("--dangerously-skip-permissions");
1040
+ if (spec.model && !head.some((a) => a === "--model" || a.startsWith("--model=")))
1041
+ lead.push("--model", spec.model);
1042
+ const pass = lead.length ? [...lead, ...spec.passthrough] : spec.passthrough;
1017
1043
  const body = systemPrompt ? mergeFragment(pass, systemPrompt, extra) : [...pass, ...extra];
1018
1044
  return [...harness, ...body];
1019
1045
  }
@@ -1023,6 +1049,19 @@ function expandGeminiModel(m) {
1023
1049
  function escapeGeminiImports(text) {
1024
1050
  return text.replaceAll("@", "\\@");
1025
1051
  }
1052
+ function trackGeminiTempDir(dir) {
1053
+ geminiTempDirs.add(dir);
1054
+ if (geminiCleanupArmed)
1055
+ return;
1056
+ geminiCleanupArmed = true;
1057
+ process.on("exit", () => {
1058
+ for (const d of geminiTempDirs) {
1059
+ try {
1060
+ rmSync3(d, { recursive: true, force: true });
1061
+ } catch {}
1062
+ }
1063
+ });
1064
+ }
1026
1065
  function geminiRenderArgs(spec) {
1027
1066
  const includes = [];
1028
1067
  const systemPrompt = joinFragments([spec.cast, spec.pointer, spec.globals]);
@@ -1030,11 +1069,7 @@ function geminiRenderArgs(spec) {
1030
1069
  const dir = mkdtempSync2(join8(tmpdir2(), "imprnt-gemini-"));
1031
1070
  writeFileSync6(join8(dir, "GEMINI.md"), escapeGeminiImports(systemPrompt) + `
1032
1071
  `);
1033
- process.on("exit", () => {
1034
- try {
1035
- rmSync3(dir, { recursive: true, force: true });
1036
- } catch {}
1037
- });
1072
+ trackGeminiTempDir(dir);
1038
1073
  includes.push(dir);
1039
1074
  }
1040
1075
  includes.push(...spec.addDirs);
@@ -1042,23 +1077,30 @@ function geminiRenderArgs(spec) {
1042
1077
  console.error("imp: gemini does not host claude harness plugins (statusline, timemachine) - skipping them");
1043
1078
  }
1044
1079
  const lead = [];
1080
+ const head = flagHead(spec.passthrough);
1045
1081
  if (spec.skipPermissions) {
1046
- if (!spec.passthrough.includes("--yolo") && !spec.passthrough.includes("-y"))
1082
+ if (!head.includes("--yolo") && !head.includes("-y"))
1047
1083
  lead.push("--yolo");
1048
- if (!spec.passthrough.includes("--skip-trust"))
1084
+ if (!head.includes("--skip-trust"))
1049
1085
  lead.push("--skip-trust");
1050
1086
  }
1051
1087
  const pass = [...spec.passthrough];
1052
- const mIdx = pass.findIndex((a) => a === "-m" || a === "--model");
1053
- if (mIdx >= 0 && pass[mIdx + 1] !== undefined) {
1054
- pass[mIdx + 1] = expandGeminiModel(pass[mIdx + 1]);
1055
- } else if (mIdx < 0 && spec.model) {
1088
+ const mEnd = flagHead(pass).length;
1089
+ const isModelFlag = (a) => a === "-m" || a === "--model" || a.startsWith("-m=") || a.startsWith("--model=");
1090
+ const mIdx = pass.slice(0, mEnd).findIndex(isModelFlag);
1091
+ if (mIdx >= 0) {
1092
+ const tok = pass[mIdx];
1093
+ const eq = tok.indexOf("=");
1094
+ if (eq >= 0)
1095
+ pass[mIdx] = tok.slice(0, eq + 1) + expandGeminiModel(tok.slice(eq + 1));
1096
+ else if (mIdx + 1 < mEnd && pass[mIdx + 1] !== undefined)
1097
+ pass[mIdx + 1] = expandGeminiModel(pass[mIdx + 1]);
1098
+ } else if (spec.model) {
1056
1099
  pass.unshift("-m", expandGeminiModel(spec.model));
1057
1100
  }
1058
- const rIdx = pass.findIndex((a) => a === "-r" || a === "--resume");
1059
- if (rIdx >= 0 && (pass[rIdx + 1] === undefined || pass[rIdx + 1].startsWith("-"))) {
1101
+ const rIdx = valuelessResumeIndex(pass);
1102
+ if (rIdx >= 0)
1060
1103
  pass.splice(rIdx + 1, 0, "latest");
1061
- }
1062
1104
  const flags = includes.length ? ["--include-directories", includes.join(",")] : [];
1063
1105
  return [...lead, ...flags, ...pass];
1064
1106
  }
@@ -1071,11 +1113,88 @@ function parseGeminiSessions(stdout) {
1071
1113
  }
1072
1114
  return out;
1073
1115
  }
1116
+ function valuelessResumeIndex(args) {
1117
+ const term = args.indexOf("--");
1118
+ const end = term < 0 ? args.length : term;
1119
+ let i = -1;
1120
+ for (let k = 0;k < end; k++) {
1121
+ if (args[k] === "-r" || args[k] === "--resume") {
1122
+ i = k;
1123
+ break;
1124
+ }
1125
+ }
1126
+ if (i < 0)
1127
+ return -1;
1128
+ const next = i + 1 < end ? args[i + 1] : undefined;
1129
+ return next === undefined || next.startsWith("-") ? i : -1;
1130
+ }
1131
+ function listGeminiSessions(cwd) {
1132
+ const r = spawnSync2("gemini", ["--list-sessions", "--skip-trust"], { cwd, encoding: "utf8" });
1133
+ if (r.error || r.status !== 0)
1134
+ return { sessions: [], ran: false };
1135
+ const stdout = r.stdout || "";
1136
+ const sessions = parseGeminiSessions(stdout);
1137
+ if (!sessions.length && stdout.trim() && !/\(\s*0\s*\)|no sessions/i.test(stdout)) {
1138
+ console.error("imp: couldn't parse gemini's session list (its format may have changed) - starting fresh");
1139
+ }
1140
+ return { sessions, ran: true };
1141
+ }
1142
+ async function pickGeminiSession(sessions, label) {
1143
+ const cols = process.stdout.columns || 80;
1144
+ const nameW = Math.max(20, Math.min(64, cols - 16));
1145
+ const trunc = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "…" : s;
1146
+ process.stderr.write(`
1147
+ resume a chat - gemini · ${label}
1148
+
1149
+ `);
1150
+ sessions.forEach((s, i) => {
1151
+ process.stderr.write(` ${String(i + 1).padStart(2)} ${trunc(s.name).padEnd(nameW)} ${s.age.replace(/\s*ago$/, "")}
1152
+ `);
1153
+ });
1154
+ process.stderr.write(`
1155
+ `);
1156
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1157
+ const answer = (await rl.question(" number to resume · enter for newest · q to cancel › ")).trim();
1158
+ rl.close();
1159
+ if (answer === "")
1160
+ return "latest";
1161
+ if (answer.toLowerCase() === "q")
1162
+ return null;
1163
+ const n = Number(answer);
1164
+ if (Number.isInteger(n) && n >= 1 && n <= sessions.length)
1165
+ return sessions[n - 1].id;
1166
+ process.stderr.write(` not a valid choice - starting a fresh session
1167
+ `);
1168
+ return null;
1169
+ }
1170
+ async function geminiResolveResume(passthrough, cwd) {
1171
+ const rIdx = valuelessResumeIndex(passthrough);
1172
+ if (rIdx < 0)
1173
+ return passthrough;
1174
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
1175
+ return passthrough;
1176
+ const { sessions, ran } = listGeminiSessions(cwd);
1177
+ if (!ran)
1178
+ return passthrough;
1179
+ if (!sessions.length) {
1180
+ console.error("imp: no saved gemini chats for this project yet - starting fresh");
1181
+ return passthrough.filter((_, i) => i !== rIdx);
1182
+ }
1183
+ const chosen = await pickGeminiSession(sessions, basename2(cwd));
1184
+ if (chosen === null)
1185
+ return passthrough.filter((_, i) => i !== rIdx);
1186
+ const out = [...passthrough];
1187
+ out.splice(rIdx + 1, 0, chosen);
1188
+ return out;
1189
+ }
1074
1190
  function resolveLaunch(passthrough, config = {}) {
1191
+ const term = passthrough.indexOf("--");
1192
+ const head = term < 0 ? passthrough : passthrough.slice(0, term);
1193
+ const tail = term < 0 ? [] : passthrough.slice(term);
1075
1194
  let agentFlag;
1076
1195
  let yoloFlag;
1077
1196
  const rest = [];
1078
- for (const a of passthrough) {
1197
+ for (const a of head) {
1079
1198
  if (a === "--gemini")
1080
1199
  agentFlag = agentFlag ?? "gemini";
1081
1200
  else if (a === "--claude")
@@ -1088,14 +1207,15 @@ function resolveLaunch(passthrough, config = {}) {
1088
1207
  rest.push(a);
1089
1208
  }
1090
1209
  const name = agentFlag ?? process.env.IMPRNT_AGENT ?? config.agent ?? "claude";
1091
- let backend = backends[name];
1210
+ let backend = Object.hasOwn(backends, name) ? backends[name] : undefined;
1092
1211
  if (!backend) {
1093
1212
  console.error(`imp: unknown agent "${name}" - falling back to claude (valid: ${Object.keys(backends).join(", ")})`);
1094
1213
  backend = claudeBackend;
1095
1214
  }
1096
- const envYolo = "IMPRNT_YOLO" in process.env ? !["0", "false", ""].includes(process.env.IMPRNT_YOLO ?? "") : undefined;
1215
+ const yoloEnv = process.env.IMPRNT_YOLO;
1216
+ const envYolo = yoloEnv === undefined ? undefined : !["0", "false", "", "off", "no"].includes(yoloEnv.toLowerCase());
1097
1217
  const skipPermissions = yoloFlag ?? envYolo ?? config.yolo ?? false;
1098
- return { backend, skipPermissions, passthrough: rest };
1218
+ return { backend, skipPermissions, passthrough: [...rest, ...tail] };
1099
1219
  }
1100
1220
  function launchBackend(backend, cwd, args, env = process.env) {
1101
1221
  if (!isDir(cwd)) {
@@ -1110,7 +1230,7 @@ function launchBackend(backend, cwd, args, env = process.env) {
1110
1230
  }
1111
1231
  return r.status ?? 1;
1112
1232
  }
1113
- var claudeBackend, GEMINI_MODEL_ALIASES, geminiBackend, backends;
1233
+ var claudeBackend, GEMINI_MODEL_ALIASES, geminiTempDirs, geminiCleanupArmed = false, geminiBackend, backends;
1114
1234
  var init_launch = __esm(() => {
1115
1235
  init_plugins();
1116
1236
  init_global();
@@ -1127,10 +1247,12 @@ var init_launch = __esm(() => {
1127
1247
  gemma31: "gemma-4-31b-it",
1128
1248
  gemma26: "gemma-4-26b-a4b-it"
1129
1249
  };
1250
+ geminiTempDirs = new Set;
1130
1251
  geminiBackend = {
1131
1252
  name: "gemini",
1132
1253
  missingHint: "Install the Gemini CLI first: npm i -g @google/gemini-cli",
1133
- renderArgs: geminiRenderArgs
1254
+ renderArgs: geminiRenderArgs,
1255
+ resolveResume: geminiResolveResume
1134
1256
  };
1135
1257
  backends = {
1136
1258
  claude: claudeBackend,
@@ -1173,7 +1295,7 @@ var init_manifest = () => {};
1173
1295
  var exports_ingest = {};
1174
1296
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync3, existsSync as existsSync9, copyFileSync, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync6 } from "node:fs";
1175
1297
  import { createHash } from "node:crypto";
1176
- import { basename as basename2, extname, join as join10, relative as relative3 } from "node:path";
1298
+ import { basename as basename3, extname, join as join10, relative as relative3 } from "node:path";
1177
1299
  function slugify(s) {
1178
1300
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
1179
1301
  }
@@ -1254,7 +1376,7 @@ function applyStaged(staged, vault) {
1254
1376
  return "error";
1255
1377
  }
1256
1378
  const title = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? "";
1257
- const slug = deriveSlug([fmScalar2(fm, "slug"), title, basename2(staged, ".md")]);
1379
+ const slug = deriveSlug([fmScalar2(fm, "slug"), title, basename3(staged, ".md")]);
1258
1380
  if (!slug) {
1259
1381
  console.error(` ✗ ${staged}: can't derive a slug - slug:, the H1 title, and the filename all slugify to nothing`);
1260
1382
  return "error";
@@ -1481,7 +1603,7 @@ ${filed} filed, ${noop} no-op, ${conflict} conflict, ${error} error.`);
1481
1603
  console.log(`unchanged (hash ${hash}) — skipping ${src}. note: ${prior.note}`);
1482
1604
  process.exit(0);
1483
1605
  }
1484
- fname = basename2(src);
1606
+ fname = basename3(src);
1485
1607
  dateMatch = fname.match(/(\d{4}-\d{2}-\d{2})/) || text.match(/^\s*date:\s*(\d{4}-\d{2}-\d{2})/im);
1486
1608
  date = dateMatch ? dateMatch[1] : new Date().toISOString().slice(0, 10);
1487
1609
  lines = text.split(/\r?\n/);
@@ -1515,7 +1637,7 @@ ${filed} filed, ${noop} no-op, ${conflict} conflict, ${error} error.`);
1515
1637
  }
1516
1638
  recurringSpeaker = [...turnsBySpeaker.values()].some((n) => n >= 2);
1517
1639
  isTranscript = isFile && looksLikeTranscript(speakers, turnCount, contentLines, recurringSpeaker);
1518
- slugBasis = slugHint || subject || [...speakers].join("-") || (isFile ? basename2(src, extname(src)) : text.slice(0, 60));
1640
+ slugBasis = slugHint || subject || [...speakers].join("-") || (isFile ? basename3(src, extname(src)) : text.slice(0, 60));
1519
1641
  subjectSlug = slugify(slugBasis) || "source";
1520
1642
  noteSlug = `${date}-${subjectSlug}`;
1521
1643
  rawDir = join10(vault, "..", "raw", isTranscript ? "transcripts" : "adhoc");
@@ -1756,7 +1878,7 @@ var init_tags = __esm(() => {
1756
1878
  // scripts/recall.ts
1757
1879
  var exports_recall = {};
1758
1880
  import { lstatSync as lstatSync2, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "node:fs";
1759
- import { basename as basename3, join as join12, relative as relative4 } from "node:path";
1881
+ import { basename as basename4, join as join12, relative as relative4 } from "node:path";
1760
1882
  function walk2(dir2) {
1761
1883
  const out = [];
1762
1884
  for (const entry of readdirSync6(dir2)) {
@@ -1973,7 +2095,7 @@ var init_recall = __esm(() => {
1973
2095
  for (const t of tokens)
1974
2096
  tf.set(t, (tf.get(t) ?? 0) + weight);
1975
2097
  };
1976
- add([...tokenize(titleText), ...tokenize(basename3(path, ".md"))], TITLE_BOOST);
2098
+ add([...tokenize(titleText), ...tokenize(basename4(path, ".md"))], TITLE_BOOST);
1977
2099
  add(tokenize(aliases), TITLE_BOOST);
1978
2100
  add(tags.flatMap(tokenize), TAG_BOOST);
1979
2101
  add(tokenize(summary), SUMMARY_BOOST);
@@ -2038,7 +2160,7 @@ var init_recall = __esm(() => {
2038
2160
  var exports_snapshot = {};
2039
2161
  import { readdirSync as readdirSync7, readFileSync as readFileSync12, copyFileSync as copyFileSync2, mkdirSync as mkdirSync4, existsSync as existsSync11, statSync as statSync7, lstatSync as lstatSync3 } from "node:fs";
2040
2162
  import { createHash as createHash2 } from "node:crypto";
2041
- import { join as join13, relative as relative5, basename as basename4, extname as extname2, resolve as resolve5, sep as sep3 } from "node:path";
2163
+ import { join as join13, relative as relative5, basename as basename5, extname as extname2, resolve as resolve5, sep as sep3 } from "node:path";
2042
2164
  function destWithinRaw(rawRoot, dest) {
2043
2165
  const rootResolved = resolve5(rawRoot);
2044
2166
  const destResolved = resolve5(rootResolved, dest);
@@ -2050,7 +2172,7 @@ function collect(p, base) {
2050
2172
  let skippedLinks = 0;
2051
2173
  const st = statSync7(p);
2052
2174
  if (st.isFile())
2053
- return { files: [{ abs: p, rel: basename4(p) }], skippedLinks };
2175
+ return { files: [{ abs: p, rel: basename5(p) }], skippedLinks };
2054
2176
  const out = [];
2055
2177
  const walk3 = (dir2) => {
2056
2178
  for (const entry of readdirSync7(dir2)) {
@@ -2221,6 +2343,13 @@ function hostMemoryStores() {
2221
2343
  }
2222
2344
  }).sort();
2223
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
+ }
2224
2353
  function syncNeedsReview(lines2) {
2225
2354
  const p = join14(vault4, "_needs-review.md");
2226
2355
  const exists = existsSync12(p);
@@ -2400,10 +2529,18 @@ var init_check = __esm(() => {
2400
2529
  for (const r of uncovered)
2401
2530
  review.push(`- [ ] unclassified snapshot \`${r}\` — no vault note points back`);
2402
2531
  memoryStores = hostMemoryStores();
2403
- strayStores = memoryStores.map((dir2) => ({
2404
- dir: dir2,
2405
- files: existsSync12(dir2) ? readdirSync8(dir2).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort() : []
2406
- })).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);
2407
2544
  strayCount = strayStores.reduce((n, s) => n + s.files.length, 0);
2408
2545
  for (const s of strayStores)
2409
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`);
@@ -2528,10 +2665,10 @@ all plugin checks passed.`);
2528
2665
  var exports_cli = {};
2529
2666
  import { cpSync as cpSync3, mkdirSync as mkdirSync5, chmodSync, existsSync as existsSync13, readFileSync as readFileSync14, lstatSync as lstatSync4, statSync as statSync9, realpathSync as realpathSync2 } from "node:fs";
2530
2667
  import { spawnSync as spawnSync4 } from "node:child_process";
2531
- import { join as join15, dirname as dirname4, resolve as resolve6, basename as basename5 } from "node:path";
2668
+ import { join as join15, dirname as dirname4, resolve as resolve6, basename as basename6 } from "node:path";
2532
2669
  import { homedir as homedir4 } from "node:os";
2533
2670
  import { fileURLToPath } from "node:url";
2534
- import { createInterface } from "node:readline/promises";
2671
+ import { createInterface as createInterface2 } from "node:readline/promises";
2535
2672
  function pluginRoot() {
2536
2673
  const local = projectRoot();
2537
2674
  if (process.env.IMPRNT_ROOT || process.env.IMPRINT_ROOT)
@@ -2578,7 +2715,7 @@ function physicalTarget(target) {
2578
2715
  const parent = dirname4(existing);
2579
2716
  if (parent === existing)
2580
2717
  return target;
2581
- tail.unshift(basename5(existing));
2718
+ tail.unshift(basename6(existing));
2582
2719
  existing = parent;
2583
2720
  }
2584
2721
  try {
@@ -2607,61 +2744,6 @@ function controlSlot(dst) {
2607
2744
  return "keep";
2608
2745
  return "blocked-non-regular-file";
2609
2746
  }
2610
- function listGeminiSessions(cwd) {
2611
- const r = spawnSync4("gemini", ["--list-sessions", "--skip-trust"], { cwd, encoding: "utf8" });
2612
- return parseGeminiSessions(r.stdout || "");
2613
- }
2614
- async function pickGeminiSession(sessions, label) {
2615
- const cols = process.stdout.columns || 80;
2616
- const nameW = Math.max(20, Math.min(64, cols - 16));
2617
- const trunc = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "…" : s;
2618
- process.stderr.write(`
2619
- resume a chat — gemini · ${label}
2620
-
2621
- `);
2622
- sessions.forEach((s, i) => {
2623
- process.stderr.write(` ${String(i + 1).padStart(2)} ${trunc(s.name).padEnd(nameW)} ${s.age.replace(/\s*ago$/, "")}
2624
- `);
2625
- });
2626
- process.stderr.write(`
2627
- `);
2628
- const rl = createInterface({ input: process.stdin, output: process.stderr });
2629
- const answer = (await rl.question(" number to resume · enter for newest · q to cancel › ")).trim();
2630
- rl.close();
2631
- if (answer === "")
2632
- return "latest";
2633
- if (answer.toLowerCase() === "q")
2634
- return null;
2635
- const n = Number(answer);
2636
- if (Number.isInteger(n) && n >= 1 && n <= sessions.length)
2637
- return sessions[n - 1].id;
2638
- process.stderr.write(` not a valid choice — starting a fresh session
2639
- `);
2640
- return null;
2641
- }
2642
- async function resolveGeminiResume(backendName, passthrough, cwd) {
2643
- if (backendName !== "gemini")
2644
- return passthrough;
2645
- const rIdx = passthrough.findIndex((a) => a === "-r" || a === "--resume");
2646
- if (rIdx < 0)
2647
- return passthrough;
2648
- const next = passthrough[rIdx + 1];
2649
- if (next !== undefined && !next.startsWith("-"))
2650
- return passthrough;
2651
- if (!(process.stdin.isTTY && process.stdout.isTTY))
2652
- return passthrough;
2653
- const sessions = listGeminiSessions(cwd);
2654
- if (!sessions.length) {
2655
- console.error("imp: no saved gemini chats for this project yet — starting fresh");
2656
- return passthrough.filter((_, i) => i !== rIdx);
2657
- }
2658
- const chosen = await pickGeminiSession(sessions, basename5(cwd));
2659
- if (chosen === null)
2660
- return passthrough.filter((_, i) => i !== rIdx);
2661
- const out = [...passthrough];
2662
- out.splice(rIdx + 1, 0, chosen);
2663
- return out;
2664
- }
2665
2747
  var here, pkgRoot, cmd, rest, asImp;
2666
2748
  var init_cli = __esm(async () => {
2667
2749
  init_resolve();
@@ -2711,8 +2793,8 @@ var init_cli = __esm(async () => {
2711
2793
  case "lair": {
2712
2794
  const home = requireVaultHome();
2713
2795
  const { backend, skipPermissions, passthrough } = resolveLaunch(rest, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
2714
- const resumed = await resolveGeminiResume(backend.name, passthrough, home);
2715
- const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
2796
+ const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, home) : passthrough;
2797
+ const spec = assembleSession({ cwd: home, vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
2716
2798
  process.exit(launchBackend(backend, home, backend.renderArgs(spec), spec.env));
2717
2799
  }
2718
2800
  case "context": {
@@ -2735,8 +2817,8 @@ var init_cli = __esm(async () => {
2735
2817
  console.log("set: imprnt agent <claude|gemini> override one session: imp --gemini | imp --claude");
2736
2818
  break;
2737
2819
  }
2738
- if (name !== "claude" && name !== "gemini") {
2739
- console.error(`unknown agent "${name}" — valid: claude, gemini`);
2820
+ if (!Object.hasOwn(backends, name)) {
2821
+ console.error(`unknown agent "${name}" — valid: ${Object.keys(backends).join(", ")}`);
2740
2822
  process.exit(1);
2741
2823
  }
2742
2824
  const r = setDefaultAgent(name);
@@ -2769,29 +2851,32 @@ var init_cli = __esm(async () => {
2769
2851
  break;
2770
2852
  }
2771
2853
  case "model": {
2854
+ const envAgent = process.env.IMPRNT_AGENT;
2855
+ const agentName = envAgent ?? readDefaultAgent() ?? "claude";
2856
+ const agent = agentName in backends ? agentName : "claude";
2772
2857
  const [arg] = rest;
2773
2858
  if (!arg) {
2774
- const m = readDefaultModel();
2775
- 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)"}`);
2776
2861
  console.log("set: imprnt model <alias|id> clear: imprnt model off");
2777
- console.log("gemini aliases: pro=gemini-3.1-pro-preview, flash=gemini-3.5-flash, pro25=gemini-2.5-pro, lite=gemini-3.1-flash-lite, gemma31, gemma26");
2862
+ console.log(`gemini aliases: ${Object.entries(GEMINI_MODEL_ALIASES).map(([k, v]) => `${k}=${v}`).join(", ")}`);
2778
2863
  break;
2779
2864
  }
2780
2865
  if (arg === "off" || arg === "default" || arg === "none") {
2781
- const r2 = setDefaultModel("");
2866
+ const r2 = setDefaultModel("", agent);
2782
2867
  if (!r2.ok) {
2783
2868
  console.error(`could not clear default model (${configPath()}): ${r2.error}`);
2784
2869
  process.exit(1);
2785
2870
  }
2786
- 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`);
2787
2872
  break;
2788
2873
  }
2789
- const r = setDefaultModel(arg);
2874
+ const r = setDefaultModel(arg, agent);
2790
2875
  if (!r.ok) {
2791
2876
  console.error(`could not set default model (${configPath()}): ${r.error}`);
2792
2877
  process.exit(1);
2793
2878
  }
2794
- console.log(`default model set to ${arg} (${configPath()})`);
2879
+ console.log(`default model for ${agent} set to ${arg} (${configPath()})`);
2795
2880
  break;
2796
2881
  }
2797
2882
  case "plugin": {
@@ -2999,7 +3084,7 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
2999
3084
  const inPlace = isVaultProject(process.cwd());
3000
3085
  const display = inPlace ? process.cwd() : "~/imprnt";
3001
3086
  const fallback = inPlace ? process.cwd() : join15(homedir4(), "imprnt");
3002
- const rl = createInterface({ input: process.stdin, output: process.stdout });
3087
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3003
3088
  const answer = (await rl.question(`vault location [${display}]: `)).trim();
3004
3089
  rl.close();
3005
3090
  target = answer ? resolvePath(answer) : fallback;
@@ -3062,7 +3147,10 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3062
3147
  added.push("CLAUDE.md");
3063
3148
  }
3064
3149
  try {
3150
+ const prevMode = statSync9(target).mode & 511;
3065
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")})`);
3066
3154
  } catch (e) {
3067
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`);
3068
3156
  }
@@ -3111,8 +3199,8 @@ enable: imprnt plugin add <name> disable: imprnt plugin rm <name> [--purge]`);
3111
3199
  }
3112
3200
  const raw = bare ? [] : [cmd, ...rest];
3113
3201
  const { backend, skipPermissions, passthrough } = resolveLaunch(raw, { agent: readDefaultAgent(), yolo: readSkipPermissions() });
3114
- const resumed = await resolveGeminiResume(backend.name, passthrough, process.cwd());
3115
- const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel() });
3202
+ const resumed = backend.resolveResume ? await backend.resolveResume(passthrough, process.cwd()) : passthrough;
3203
+ const spec = assembleSession({ cwd: process.cwd(), vaultProject: home, pkgRoot, passthrough: resumed, skipPermissions, model: readDefaultModel(backend.name) });
3116
3204
  process.exit(launchBackend(backend, process.cwd(), backend.renderArgs(spec), spec.env));
3117
3205
  }
3118
3206
  console.log(`imprnt — deterministic-first markdown knowledge vault
@@ -3132,7 +3220,7 @@ engine (same subcommands under \`imp\` or \`imprnt\`):
3132
3220
  imprnt context print the vault contract — agents run this before writing any note
3133
3221
  imprnt agent [claude|gemini] show or set the per-machine default agent backend for imp
3134
3222
  imprnt yolo [on|off] show or set the per-machine skip-permissions default for imp
3135
- 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, ...)
3136
3224
  imprnt check [--all] [--vault D] integrity (orphan links, disconnected notes, uncovered snapshots) + regenerate index.md; --all also runs each plugins/*/check.js
3137
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/
3138
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.60",
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": {