imprnt 0.1.2 → 0.1.3-edge.12
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 +3 -1
- package/README.md +3 -16
- package/dist/cli.js +69 -5
- package/dist/imp.js +69 -5
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -166,8 +166,10 @@ Core is the vault + `ingest → recall → check`. Everything else (a task-mirro
|
|
|
166
166
|
- **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.
|
|
167
167
|
- **`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).
|
|
168
168
|
- **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.)
|
|
169
|
+
- **Harness plugins** are the third class: they customize Claude Code itself, never the vault (guard's PreToolUse 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.
|
|
170
|
+
- **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.
|
|
169
171
|
- **Commands only, no forced daemon.** The user schedules sync. Install/remove is manual (README `## Install` / `## Remove` + `rm -rf`), no registry.
|
|
170
|
-
- **Core ↔ plugin contact = exactly
|
|
172
|
+
- **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.)
|
|
171
173
|
|
|
172
174
|
## Out of scope (on purpose)
|
|
173
175
|
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
|
@@ -67,21 +67,6 @@ imprnt init # scaffold your vault, drop CLAUDE.md (the contract your
|
|
|
67
67
|
imp # open your assistant and talk
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
> **Temporarily off npm (June 2026).** The package is unpublished while the installer gets a final
|
|
71
|
-
> polish, so `npm i -g imprnt` 404s right now. Until it returns, install from source (needs
|
|
72
|
-
> [Bun](https://bun.sh) to build):
|
|
73
|
-
>
|
|
74
|
-
> ```sh
|
|
75
|
-
> git clone https://github.com/aleksandr-bogdanov/imprnt
|
|
76
|
-
> cd imprnt && bun install && bun run build
|
|
77
|
-
> (cd packages/imprnt && bun run shipdocs) # stage the CLAUDE.md contract the install ships
|
|
78
|
-
> npm i -g ./packages/imprnt # the same imprnt + imp commands, from your clone
|
|
79
|
-
> ```
|
|
80
|
-
>
|
|
81
|
-
> The `shipdocs` step is what npm's publish runs for you. From source you run it once by hand so
|
|
82
|
-
> `imprnt init` has the contract to drop. The global install symlinks to your clone, so keep the
|
|
83
|
-
> clone in place.
|
|
84
|
-
|
|
85
70
|
`imprnt init` scaffolds the vault, writes the `CLAUDE.md` contract that teaches your assistant
|
|
86
71
|
how it works, and registers the folder so `imp` finds it from anywhere. Runs on
|
|
87
72
|
[Node](https://nodejs.org) 18 or newer, driving [Claude Code](https://claude.com/claude-code) as
|
|
@@ -127,7 +112,9 @@ asking ("add the anti-slop plugin"), each a separate `imprnt-plugin-*` package:
|
|
|
127
112
|
| `imprnt-plugin-character` | A voice and standards to write in. "Scribe" is the default you copy and personalize. |
|
|
128
113
|
| `imprnt-plugin-anti-slop` | Rules that keep its prose from reading like AI. |
|
|
129
114
|
| `imprnt-plugin-whenful` | A local mirror of your [Whenful](https://whenful.com) tasks, shown inline at read. |
|
|
130
|
-
| `imprnt-plugin-guard` | A
|
|
115
|
+
| `imprnt-plugin-guard` | A hook that blocks dangerous shell commands before they run. |
|
|
116
|
+
| `imprnt-plugin-statusline` | A customizable status line: model, branch, context, cost, rate limits, clock. |
|
|
117
|
+
| `imprnt-plugin-telegram` | Your vault from your phone: text a bot, the answer comes from your notes. |
|
|
131
118
|
|
|
132
119
|
Adding one copies it into your project and wires it into `CLAUDE.local.md`, the per-machine file
|
|
133
120
|
your assistant loads each session. A fresh setup loads zero plugins until you add them. The full
|
package/dist/cli.js
CHANGED
|
@@ -153,6 +153,15 @@ function isEnabled(root, name) {
|
|
|
153
153
|
const prefix = `@plugins/${name}/`;
|
|
154
154
|
return liveImportLines(root).some((l) => l.startsWith(prefix));
|
|
155
155
|
}
|
|
156
|
+
function enabledPluginDirs(root) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const target of importTargets(root)) {
|
|
159
|
+
const name = target.match(/^plugins\/([^/]+)\//)?.[1];
|
|
160
|
+
if (name && !out.includes(name))
|
|
161
|
+
out.push(name);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
156
165
|
function addPlugin(root, spec) {
|
|
157
166
|
const invalid = specError(root, spec);
|
|
158
167
|
if (invalid)
|
|
@@ -293,7 +302,7 @@ function purgePlugin(projectRoot, name) {
|
|
|
293
302
|
var OFFICIAL;
|
|
294
303
|
var init_install = __esm(() => {
|
|
295
304
|
init_plugins();
|
|
296
|
-
OFFICIAL = ["anti-slop", "character", "whenful", "guard"];
|
|
305
|
+
OFFICIAL = ["anti-slop", "character", "whenful", "guard", "statusline", "telegram"];
|
|
297
306
|
});
|
|
298
307
|
|
|
299
308
|
// scripts/lib/roots.ts
|
|
@@ -607,6 +616,49 @@ function pointerFragment(pkgRoot, vaultProject) {
|
|
|
607
616
|
const tpl = readFileSync6(join7(pkgRoot, "templates", "pointer.md"), "utf8");
|
|
608
617
|
return tpl.replaceAll("{{VAULT_PROJECT}}", () => vaultProject);
|
|
609
618
|
}
|
|
619
|
+
function substitutePluginDir(value, dir) {
|
|
620
|
+
if (typeof value === "string")
|
|
621
|
+
return value.replaceAll("${PLUGIN_DIR}", dir).replaceAll("${CLAUDE_PLUGIN_ROOT}", dir);
|
|
622
|
+
if (Array.isArray(value))
|
|
623
|
+
return value.map((v) => substitutePluginDir(v, dir));
|
|
624
|
+
if (value && typeof value === "object") {
|
|
625
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, substitutePluginDir(v, dir)]));
|
|
626
|
+
}
|
|
627
|
+
return value;
|
|
628
|
+
}
|
|
629
|
+
function isPlainObject(v) {
|
|
630
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
631
|
+
}
|
|
632
|
+
function mergeSettings(a, b) {
|
|
633
|
+
const out = { ...a };
|
|
634
|
+
for (const [k, v] of Object.entries(b)) {
|
|
635
|
+
const prev = out[k];
|
|
636
|
+
out[k] = isPlainObject(prev) && isPlainObject(v) ? mergeSettings(prev, v) : v;
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
function harnessFlags(root) {
|
|
641
|
+
const flags = [];
|
|
642
|
+
let settings = {};
|
|
643
|
+
for (const name of enabledPluginDirs(root)) {
|
|
644
|
+
const dir = join7(root, "plugins", name);
|
|
645
|
+
if (entryExists(root, `plugins/${name}/.claude-plugin/plugin.json`))
|
|
646
|
+
flags.push("--plugin-dir", dir);
|
|
647
|
+
if (!entryExists(root, `plugins/${name}/imp-settings.json`))
|
|
648
|
+
continue;
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(readFileSync6(join7(dir, "imp-settings.json"), "utf8"));
|
|
651
|
+
if (!isPlainObject(parsed))
|
|
652
|
+
throw new Error("not a JSON object");
|
|
653
|
+
settings = mergeSettings(settings, substitutePluginDir(parsed, dir));
|
|
654
|
+
} catch (e) {
|
|
655
|
+
console.error(`imp: skipping bad settings fragment plugins/${name}/imp-settings.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (Object.keys(settings).length)
|
|
659
|
+
flags.push("--settings", JSON.stringify(settings));
|
|
660
|
+
return flags;
|
|
661
|
+
}
|
|
610
662
|
function realResolve(p) {
|
|
611
663
|
try {
|
|
612
664
|
return realpathSync(p);
|
|
@@ -637,8 +689,9 @@ function buildLaunch(opts) {
|
|
|
637
689
|
console.error(`imp: vault project not found at ${opts.vaultProject} - launching plain claude (re-run \`imprnt init\` there, or fix IMPRNT_ROOT)`);
|
|
638
690
|
return { args: pass, env: process.env };
|
|
639
691
|
}
|
|
692
|
+
const harness = harnessFlags(opts.vaultProject);
|
|
640
693
|
if (isInside(opts.cwd, opts.vaultProject))
|
|
641
|
-
return { args: pass, env: childEnv(opts.vaultProject) };
|
|
694
|
+
return { args: [...harness, ...pass], env: childEnv(opts.vaultProject) };
|
|
642
695
|
const fragment = [castFragment(opts.vaultProject), pointerFragment(opts.pkgRoot, opts.vaultProject)].filter(Boolean).join(`
|
|
643
696
|
|
|
644
697
|
`);
|
|
@@ -667,7 +720,7 @@ function buildLaunch(opts) {
|
|
|
667
720
|
args.splice(term, 0, ...inject);
|
|
668
721
|
else
|
|
669
722
|
args.push(...inject);
|
|
670
|
-
return { args, env: childEnv(opts.vaultProject) };
|
|
723
|
+
return { args: [...harness, ...args], env: childEnv(opts.vaultProject) };
|
|
671
724
|
}
|
|
672
725
|
function launchClaude(cwd, args, env = process.env) {
|
|
673
726
|
if (!isDir(cwd)) {
|
|
@@ -2035,6 +2088,14 @@ import { join as join14, dirname as dirname4, resolve as resolve6 } from "node:p
|
|
|
2035
2088
|
import { homedir as homedir3 } from "node:os";
|
|
2036
2089
|
import { fileURLToPath } from "node:url";
|
|
2037
2090
|
import { createInterface } from "node:readline/promises";
|
|
2091
|
+
function pluginRoot() {
|
|
2092
|
+
const local = projectRoot();
|
|
2093
|
+
if (process.env.IMPRNT_ROOT || process.env.IMPRINT_ROOT)
|
|
2094
|
+
return local;
|
|
2095
|
+
if (existsSync12(join14(local, "vault")) || existsSync12(join14(local, "CLAUDE.local.md")))
|
|
2096
|
+
return local;
|
|
2097
|
+
return registeredRoot() ?? local;
|
|
2098
|
+
}
|
|
2038
2099
|
function vaultArg() {
|
|
2039
2100
|
const i = rest.indexOf("--vault");
|
|
2040
2101
|
if (i >= 0) {
|
|
@@ -2113,7 +2174,8 @@ var init_cli = __esm(async () => {
|
|
|
2113
2174
|
}
|
|
2114
2175
|
case "lair": {
|
|
2115
2176
|
const home = requireVaultHome();
|
|
2116
|
-
|
|
2177
|
+
const lair = buildLaunch({ cwd: home, vaultProject: home, pkgRoot, passthrough: rest });
|
|
2178
|
+
process.exit(launchClaude(home, lair.args, lair.env));
|
|
2117
2179
|
}
|
|
2118
2180
|
case "context": {
|
|
2119
2181
|
const home = requireVaultHome();
|
|
@@ -2126,7 +2188,9 @@ var init_cli = __esm(async () => {
|
|
|
2126
2188
|
break;
|
|
2127
2189
|
}
|
|
2128
2190
|
case "plugin": {
|
|
2129
|
-
const proj =
|
|
2191
|
+
const proj = pluginRoot();
|
|
2192
|
+
if (proj !== process.cwd())
|
|
2193
|
+
console.error(`(targeting vault project: ${proj})`);
|
|
2130
2194
|
const [sub, ...specs] = rest;
|
|
2131
2195
|
if (sub === "list") {
|
|
2132
2196
|
const dirs = listPluginDirs(proj);
|
package/dist/imp.js
CHANGED
|
@@ -153,6 +153,15 @@ function isEnabled(root, name) {
|
|
|
153
153
|
const prefix = `@plugins/${name}/`;
|
|
154
154
|
return liveImportLines(root).some((l) => l.startsWith(prefix));
|
|
155
155
|
}
|
|
156
|
+
function enabledPluginDirs(root) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const target of importTargets(root)) {
|
|
159
|
+
const name = target.match(/^plugins\/([^/]+)\//)?.[1];
|
|
160
|
+
if (name && !out.includes(name))
|
|
161
|
+
out.push(name);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
156
165
|
function addPlugin(root, spec) {
|
|
157
166
|
const invalid = specError(root, spec);
|
|
158
167
|
if (invalid)
|
|
@@ -293,7 +302,7 @@ function purgePlugin(projectRoot, name) {
|
|
|
293
302
|
var OFFICIAL;
|
|
294
303
|
var init_install = __esm(() => {
|
|
295
304
|
init_plugins();
|
|
296
|
-
OFFICIAL = ["anti-slop", "character", "whenful", "guard"];
|
|
305
|
+
OFFICIAL = ["anti-slop", "character", "whenful", "guard", "statusline", "telegram"];
|
|
297
306
|
});
|
|
298
307
|
|
|
299
308
|
// scripts/lib/roots.ts
|
|
@@ -607,6 +616,49 @@ function pointerFragment(pkgRoot, vaultProject) {
|
|
|
607
616
|
const tpl = readFileSync6(join7(pkgRoot, "templates", "pointer.md"), "utf8");
|
|
608
617
|
return tpl.replaceAll("{{VAULT_PROJECT}}", () => vaultProject);
|
|
609
618
|
}
|
|
619
|
+
function substitutePluginDir(value, dir) {
|
|
620
|
+
if (typeof value === "string")
|
|
621
|
+
return value.replaceAll("${PLUGIN_DIR}", dir).replaceAll("${CLAUDE_PLUGIN_ROOT}", dir);
|
|
622
|
+
if (Array.isArray(value))
|
|
623
|
+
return value.map((v) => substitutePluginDir(v, dir));
|
|
624
|
+
if (value && typeof value === "object") {
|
|
625
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, substitutePluginDir(v, dir)]));
|
|
626
|
+
}
|
|
627
|
+
return value;
|
|
628
|
+
}
|
|
629
|
+
function isPlainObject(v) {
|
|
630
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
631
|
+
}
|
|
632
|
+
function mergeSettings(a, b) {
|
|
633
|
+
const out = { ...a };
|
|
634
|
+
for (const [k, v] of Object.entries(b)) {
|
|
635
|
+
const prev = out[k];
|
|
636
|
+
out[k] = isPlainObject(prev) && isPlainObject(v) ? mergeSettings(prev, v) : v;
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
function harnessFlags(root) {
|
|
641
|
+
const flags = [];
|
|
642
|
+
let settings = {};
|
|
643
|
+
for (const name of enabledPluginDirs(root)) {
|
|
644
|
+
const dir = join7(root, "plugins", name);
|
|
645
|
+
if (entryExists(root, `plugins/${name}/.claude-plugin/plugin.json`))
|
|
646
|
+
flags.push("--plugin-dir", dir);
|
|
647
|
+
if (!entryExists(root, `plugins/${name}/imp-settings.json`))
|
|
648
|
+
continue;
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(readFileSync6(join7(dir, "imp-settings.json"), "utf8"));
|
|
651
|
+
if (!isPlainObject(parsed))
|
|
652
|
+
throw new Error("not a JSON object");
|
|
653
|
+
settings = mergeSettings(settings, substitutePluginDir(parsed, dir));
|
|
654
|
+
} catch (e) {
|
|
655
|
+
console.error(`imp: skipping bad settings fragment plugins/${name}/imp-settings.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (Object.keys(settings).length)
|
|
659
|
+
flags.push("--settings", JSON.stringify(settings));
|
|
660
|
+
return flags;
|
|
661
|
+
}
|
|
610
662
|
function realResolve(p) {
|
|
611
663
|
try {
|
|
612
664
|
return realpathSync(p);
|
|
@@ -637,8 +689,9 @@ function buildLaunch(opts) {
|
|
|
637
689
|
console.error(`imp: vault project not found at ${opts.vaultProject} - launching plain claude (re-run \`imprnt init\` there, or fix IMPRNT_ROOT)`);
|
|
638
690
|
return { args: pass, env: process.env };
|
|
639
691
|
}
|
|
692
|
+
const harness = harnessFlags(opts.vaultProject);
|
|
640
693
|
if (isInside(opts.cwd, opts.vaultProject))
|
|
641
|
-
return { args: pass, env: childEnv(opts.vaultProject) };
|
|
694
|
+
return { args: [...harness, ...pass], env: childEnv(opts.vaultProject) };
|
|
642
695
|
const fragment = [castFragment(opts.vaultProject), pointerFragment(opts.pkgRoot, opts.vaultProject)].filter(Boolean).join(`
|
|
643
696
|
|
|
644
697
|
`);
|
|
@@ -667,7 +720,7 @@ function buildLaunch(opts) {
|
|
|
667
720
|
args.splice(term, 0, ...inject);
|
|
668
721
|
else
|
|
669
722
|
args.push(...inject);
|
|
670
|
-
return { args, env: childEnv(opts.vaultProject) };
|
|
723
|
+
return { args: [...harness, ...args], env: childEnv(opts.vaultProject) };
|
|
671
724
|
}
|
|
672
725
|
function launchClaude(cwd, args, env = process.env) {
|
|
673
726
|
if (!isDir(cwd)) {
|
|
@@ -2035,6 +2088,14 @@ import { join as join14, dirname as dirname4, resolve as resolve6 } from "node:p
|
|
|
2035
2088
|
import { homedir as homedir3 } from "node:os";
|
|
2036
2089
|
import { fileURLToPath } from "node:url";
|
|
2037
2090
|
import { createInterface } from "node:readline/promises";
|
|
2091
|
+
function pluginRoot() {
|
|
2092
|
+
const local = projectRoot();
|
|
2093
|
+
if (process.env.IMPRNT_ROOT || process.env.IMPRINT_ROOT)
|
|
2094
|
+
return local;
|
|
2095
|
+
if (existsSync12(join14(local, "vault")) || existsSync12(join14(local, "CLAUDE.local.md")))
|
|
2096
|
+
return local;
|
|
2097
|
+
return registeredRoot() ?? local;
|
|
2098
|
+
}
|
|
2038
2099
|
function vaultArg() {
|
|
2039
2100
|
const i = rest.indexOf("--vault");
|
|
2040
2101
|
if (i >= 0) {
|
|
@@ -2113,7 +2174,8 @@ var init_cli = __esm(async () => {
|
|
|
2113
2174
|
}
|
|
2114
2175
|
case "lair": {
|
|
2115
2176
|
const home = requireVaultHome();
|
|
2116
|
-
|
|
2177
|
+
const lair = buildLaunch({ cwd: home, vaultProject: home, pkgRoot, passthrough: rest });
|
|
2178
|
+
process.exit(launchClaude(home, lair.args, lair.env));
|
|
2117
2179
|
}
|
|
2118
2180
|
case "context": {
|
|
2119
2181
|
const home = requireVaultHome();
|
|
@@ -2126,7 +2188,9 @@ var init_cli = __esm(async () => {
|
|
|
2126
2188
|
break;
|
|
2127
2189
|
}
|
|
2128
2190
|
case "plugin": {
|
|
2129
|
-
const proj =
|
|
2191
|
+
const proj = pluginRoot();
|
|
2192
|
+
if (proj !== process.cwd())
|
|
2193
|
+
console.error(`(targeting vault project: ${proj})`);
|
|
2130
2194
|
const [sub, ...specs] = rest;
|
|
2131
2195
|
if (sub === "list") {
|
|
2132
2196
|
const dirs = listPluginDirs(proj);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprnt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3-edge.12",
|
|
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": {
|