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 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 two convention-based aggregators**, both 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/`. Both 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.)
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 deterministic blocklist for dangerous shell commands. |
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
- process.exit(launchClaude(home, rest, childEnv(home)));
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 = projectRoot();
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
- process.exit(launchClaude(home, rest, childEnv(home)));
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 = projectRoot();
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.2",
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": {