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 +4 -4
- package/README.md +4 -4
- package/dist/cli.js +208 -120
- package/dist/imp.js +208 -120
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
178
|
-
[
|
|
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
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
1082
|
+
if (!head.includes("--yolo") && !head.includes("-y"))
|
|
1047
1083
|
lead.push("--yolo");
|
|
1048
|
-
if (!
|
|
1084
|
+
if (!head.includes("--skip-trust"))
|
|
1049
1085
|
lead.push("--skip-trust");
|
|
1050
1086
|
}
|
|
1051
1087
|
const pass = [...spec.passthrough];
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
|
1059
|
-
if (rIdx >= 0
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
2739
|
-
console.error(`unknown agent "${name}" — valid:
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
1082
|
+
if (!head.includes("--yolo") && !head.includes("-y"))
|
|
1047
1083
|
lead.push("--yolo");
|
|
1048
|
-
if (!
|
|
1084
|
+
if (!head.includes("--skip-trust"))
|
|
1049
1085
|
lead.push("--skip-trust");
|
|
1050
1086
|
}
|
|
1051
1087
|
const pass = [...spec.passthrough];
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
|
1059
|
-
if (rIdx >= 0
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
2739
|
-
console.error(`unknown agent "${name}" — valid:
|
|
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(
|
|
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 =
|
|
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
|
|
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.
|
|
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": {
|