llm-wiki-kit 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -84,7 +84,9 @@ If you need to think about saving every answer manually, the setup has failed.
84
84
  llm-wiki install --workspace /path/to/project --profile standard
85
85
  llm-wiki doctor
86
86
  llm-wiki status --workspace /path/to/project
87
+ llm-wiki projects --workspace /path/to/search-root
87
88
  llm-wiki update --check --workspace /path/to/project
89
+ llm-wiki update --all --workspace /path/to/search-root
88
90
  llm-wiki update --dry-run --workspace /path/to/project
89
91
  llm-wiki update --workspace /path/to/project
90
92
  llm-wiki uninstall
@@ -96,6 +98,10 @@ llm-wiki uninstall
96
98
 
97
99
  `llm-wiki update` upgrades the global npm package, reinstalls the hook entries, and patches only managed project files such as the marked `AGENTS.md` policy block and generated `llm-wiki/AGENTS.md`/procedure files. Existing wiki content is not overwritten.
98
100
 
101
+ `llm-wiki projects --workspace /apps` lists project roots that already have `llm-wiki-kit` state and shows the update commands to run. `llm-wiki update --all --workspace /apps` updates the global runtime once, then reapplies managed templates across every discovered project root under `/apps`.
102
+
103
+ After a plain `npm install -g llm-wiki-kit@latest`, installed hooks point at the same global binary path. The next `SessionStart`/`InstructionsLoaded` hook automatically reapplies safe managed template updates for the active project root. This only patches generated/managed files and does not overwrite wiki content.
104
+
99
105
  Real `update --check` and `update` require the package to exist in the npm registry. Before publication, use local tarball installs for smoke testing and fake npm in automated tests.
100
106
 
101
107
  The hook subcommands are internal runtime targets:
@@ -67,7 +67,9 @@ Do not delete a project `llm-wiki/` tree to reinstall the runtime. `llm-wiki uni
67
67
 
68
68
  ```bash
69
69
  llm-wiki status --workspace /path/to/project
70
+ llm-wiki projects --workspace /path/to/search-root
70
71
  llm-wiki update --check --workspace /path/to/project
72
+ llm-wiki update --all --workspace /path/to/search-root
71
73
  llm-wiki update --dry-run --workspace /path/to/project
72
74
  llm-wiki update --workspace /path/to/project
73
75
  ```
@@ -81,6 +83,8 @@ llm-wiki update --workspace /path/to/project
81
83
 
82
84
  `update --check` is online and asks npm whether a newer package version exists.
83
85
 
86
+ `projects --workspace <search-root>` lists discovered project roots that have `llm-wiki/.kit-state.json`, reports whether their managed templates are current, and prints the update commands for the search root.
87
+
84
88
  `update` applies changes explicitly only when the user runs it:
85
89
 
86
90
  - runs `npm install -g llm-wiki-kit@<target>`
@@ -89,6 +93,10 @@ llm-wiki update --workspace /path/to/project
89
93
  - patches only managed project files
90
94
  - backs up changed files under `~/.local/share/llm-wiki-kit/backups/`
91
95
 
96
+ `update --all --workspace <search-root>` performs the npm runtime update once, then reapplies managed templates to every discovered project root under the search root.
97
+
98
+ After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep pointing at the global package path. On the next `SessionStart` or `InstructionsLoaded`, the runtime automatically reapplies safe managed template updates for the active project root.
99
+
92
100
  Managed project files are conservative:
93
101
 
94
102
  - root `AGENTS.md` is patched only inside the `llm-wiki-kit` marker block
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-wiki-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Hook-first living LLM Wiki runtime for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "files": [
package/src/cli.js CHANGED
@@ -2,6 +2,8 @@ import { resolve } from 'path';
2
2
  import { handleHook } from './hook.js';
3
3
  import { install, status, uninstall } from './install.js';
4
4
  import { bootstrapProject } from './project.js';
5
+ import { inspectProjectState } from './project-state.js';
6
+ import { commandForProject, knownProjectRoots } from './projects.js';
5
7
  import { formatDoctor, runDoctor } from './doctor.js';
6
8
  import { migrate } from './migrate.js';
7
9
  import { postUpdate, update } from './update.js';
@@ -29,6 +31,8 @@ function parseOptions(args) {
29
31
  options.dryRun = true;
30
32
  } else if (arg === '--replace-hooks') {
31
33
  options.replaceHooks = true;
34
+ } else if (arg === '--all') {
35
+ options.all = true;
32
36
  } else if (arg === '--json') {
33
37
  options.json = true;
34
38
  } else {
@@ -59,6 +63,7 @@ Usage:
59
63
  llm-wiki install --workspace /apps [--profile standard]
60
64
  llm-wiki update --workspace <project> [--check|--dry-run|--to <version-or-tag>]
61
65
  llm-wiki doctor
66
+ llm-wiki projects --workspace /apps
62
67
  llm-wiki status
63
68
  llm-wiki uninstall
64
69
  llm-wiki hook codex <EventName>
@@ -92,6 +97,12 @@ Usage:
92
97
  return;
93
98
  }
94
99
 
100
+ if (command === 'projects') {
101
+ const result = await listProjects(options);
102
+ printJsonOrText(result, options, formatProjects);
103
+ return;
104
+ }
105
+
95
106
  if (command === 'update') {
96
107
  printJsonOrText(await update(options), options, formatUpdate);
97
108
  return;
@@ -132,6 +143,28 @@ Usage:
132
143
  throw new Error(`unknown command: ${command}`);
133
144
  }
134
145
 
146
+ async function listProjects(options) {
147
+ const workspace = resolve(options.workspace || process.cwd());
148
+ const roots = await knownProjectRoots({ workspace });
149
+ const projects = [];
150
+ for (const root of roots) {
151
+ let state = null;
152
+ let error = null;
153
+ try {
154
+ state = await inspectProjectState(root);
155
+ } catch (err) {
156
+ error = err.message;
157
+ }
158
+ projects.push({
159
+ workspace: root,
160
+ runtime: state?.lastRuntimeVersionApplied || 'unknown',
161
+ current: Boolean(state?.runtimeUpToDateWithProject && state?.managedFilesCurrent),
162
+ error,
163
+ });
164
+ }
165
+ return { workspace, projects };
166
+ }
167
+
135
168
  function formatStatus(value) {
136
169
  const project = value.project || {};
137
170
  return [
@@ -171,10 +204,21 @@ function formatUpdate(value) {
171
204
  `- workspace: ${value.workspace}`,
172
205
  `- before: ${value.before}`,
173
206
  `- target: ${value.target}`,
207
+ ...(value.projects ? [`- projects updated: ${value.projects.length}`] : []),
174
208
  ].join('\n');
175
209
  }
176
210
 
177
211
  function formatPostUpdate(value) {
212
+ if (Array.isArray(value.projects)) {
213
+ return [
214
+ 'llm-wiki-kit post-update complete',
215
+ `- workspace: ${value.workspace}`,
216
+ `- runtime: ${value.runtimeVersion}`,
217
+ `- changed hooks: ${value.install.changed.length ? value.install.changed.join(', ') : 'none'}`,
218
+ `- projects updated: ${value.projects.length}`,
219
+ ...value.projects.map((item) => ` - ${item.workspace}: changed ${item.project?.changed?.length || 0}, skipped ${item.project?.skipped?.length || 0}`),
220
+ ].join('\n');
221
+ }
178
222
  return [
179
223
  'llm-wiki-kit post-update complete',
180
224
  `- workspace: ${value.workspace}`,
@@ -183,3 +227,21 @@ function formatPostUpdate(value) {
183
227
  `- changed project templates: ${value.project?.changed?.length || 0}`,
184
228
  ].join('\n');
185
229
  }
230
+
231
+ function formatProjects(value) {
232
+ const lines = [
233
+ 'llm-wiki-kit projects',
234
+ `- search root: ${value.workspace}`,
235
+ ];
236
+ if (value.projects.length === 0) {
237
+ lines.push('- no applied project roots found');
238
+ } else {
239
+ for (const project of value.projects) {
240
+ const status = project.error ? `error: ${project.error}` : project.current ? 'current' : `needs update (applied ${project.runtime})`;
241
+ lines.push(`- ${project.workspace}: ${status}`);
242
+ }
243
+ }
244
+ lines.push('', 'To update every listed project root:', commandForProject('update --all', value.workspace));
245
+ lines.push('To reapply templates without npm install:', commandForProject('post-update --all', value.workspace));
246
+ return lines.join('\n');
247
+ }
package/src/fs-utils.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  unlink,
13
13
  writeFile,
14
14
  } from 'fs/promises';
15
- import { dirname, join, parse, resolve } from 'path';
15
+ import { basename, dirname, join, parse, resolve } from 'path';
16
16
  import { PROJECT_MARKERS } from './constants.js';
17
17
  import { normalizeForStorage } from './redaction.js';
18
18
 
@@ -112,8 +112,27 @@ export async function safeSymlink(target, linkPath) {
112
112
  await symlink(target, linkPath);
113
113
  }
114
114
 
115
+ async function containingWikiParent(path) {
116
+ let current = path;
117
+ while (true) {
118
+ if (
119
+ basename(current) === 'llm-wiki' &&
120
+ await exists(join(current, 'wiki', 'index.md')) &&
121
+ await exists(join(current, 'raw')) &&
122
+ await exists(join(current, 'outputs'))
123
+ ) {
124
+ return dirname(current);
125
+ }
126
+ const parent = dirname(current);
127
+ if (parent === current) return null;
128
+ current = parent;
129
+ }
130
+ }
131
+
115
132
  export async function findProjectRoot(startDir = process.cwd()) {
116
133
  let current = resolve(startDir);
134
+ const wikiParent = await containingWikiParent(current);
135
+ if (wikiParent) return wikiParent;
117
136
  while (true) {
118
137
  for (const marker of PROJECT_MARKERS) {
119
138
  if (await exists(join(current, marker))) return current;
package/src/hook.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { findProjectRoot } from './fs-utils.js';
2
2
  import { bootstrapProject, appendContextNote, appendLiveQa, appendSessionEnvelope, appendWikiLog, buildContextBrief, writeDecisionPage, writeQueryPage } from './project.js';
3
+ import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
4
+ import { recordProject } from './projects.js';
3
5
  import { summarizeForStorage } from './redaction.js';
4
6
  import { buildEntryFromState, rememberQuestion, rememberTool } from './state.js';
5
7
 
@@ -37,6 +39,15 @@ function contextOutput(eventName, context) {
37
39
  };
38
40
  }
39
41
 
42
+ async function autoUpdateManagedProject(projectRoot, eventName) {
43
+ if (process.env.LLM_WIKI_KIT_AUTO_PROJECT_UPDATE === '0') return;
44
+ if (eventName !== 'SessionStart' && eventName !== 'InstructionsLoaded') return;
45
+ const state = await inspectProjectState(projectRoot);
46
+ if (state.runtimeUpToDateWithProject && state.managedFilesCurrent) return;
47
+ const result = await applyProjectTemplateUpdate(projectRoot);
48
+ await appendWikiLog(projectRoot, `llm-wiki-kit auto-update applied runtime ${result.runtimeVersion}; changed templates: ${result.changed.length}; skipped templates: ${result.skipped.length}`);
49
+ }
50
+
40
51
  export async function handleHook(provider, explicitEvent) {
41
52
  const payload = await readStdinJson();
42
53
  payload.__provider = provider;
@@ -44,6 +55,8 @@ export async function handleHook(provider, explicitEvent) {
44
55
  const cwd = payload.cwd || process.cwd();
45
56
  const projectRoot = await findProjectRoot(cwd);
46
57
  await bootstrapProject(projectRoot);
58
+ await recordProject(projectRoot, 'hook').catch(() => {});
59
+ await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
47
60
  await appendSessionEnvelope(projectRoot, eventName, payload).catch(() => {});
48
61
 
49
62
  if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded') {
package/src/install.js CHANGED
@@ -4,6 +4,7 @@ import { CLAUDE_EVENTS, CODEX_EVENTS, KIT_NAME } from './constants.js';
4
4
  import { backupFile, ensureDir, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
5
5
  import { inspectProjectState } from './project-state.js';
6
6
  import { bootstrapProject } from './project.js';
7
+ import { recordProject } from './projects.js';
7
8
  import { binPath, detectInstallSource, packageRoot, runtimeVersion } from './version.js';
8
9
 
9
10
  function shellQuote(value) {
@@ -59,6 +60,7 @@ export async function install(options = {}) {
59
60
  }
60
61
  await safeSymlink(binPath, localBinPath);
61
62
  await bootstrapProject(workspace, { profile: options.profile || 'standard', recordState: true });
63
+ await recordProject(workspace, 'install');
62
64
 
63
65
  const codexHooksPath = join(homeDir(), '.codex', 'hooks.json');
64
66
  const claudeSettingsPath = join(homeDir(), '.claude', 'settings.json');
@@ -0,0 +1,104 @@
1
+ import { readdir } from 'fs/promises';
2
+ import { join, resolve } from 'path';
3
+ import { exists, isDirectory, kitDataDir, readJson, writeJson } from './fs-utils.js';
4
+
5
+ const REGISTRY_SCHEMA_VERSION = 1;
6
+ const PROJECTS_FILE = 'projects.json';
7
+ const SKIP_DIRS = new Set(['.cache', '.git', 'node_modules', 'llm-wiki']);
8
+
9
+ function registryPath() {
10
+ return join(kitDataDir(), PROJECTS_FILE);
11
+ }
12
+
13
+ function emptyRegistry() {
14
+ return {
15
+ schemaVersion: REGISTRY_SCHEMA_VERSION,
16
+ projects: {},
17
+ };
18
+ }
19
+
20
+ function normalizeRegistry(raw) {
21
+ return {
22
+ ...emptyRegistry(),
23
+ ...(raw || {}),
24
+ projects: {
25
+ ...((raw && raw.projects) || {}),
26
+ },
27
+ };
28
+ }
29
+
30
+ export async function readProjectRegistry() {
31
+ return normalizeRegistry(await readJson(registryPath(), null));
32
+ }
33
+
34
+ export async function writeProjectRegistry(registry) {
35
+ await writeJson(registryPath(), normalizeRegistry(registry));
36
+ }
37
+
38
+ export async function recordProject(projectRoot, source = 'unknown') {
39
+ const workspace = resolve(projectRoot);
40
+ const registry = await readProjectRegistry();
41
+ const previous = registry.projects[workspace] || {};
42
+ const now = new Date().toISOString();
43
+ registry.projects[workspace] = {
44
+ workspace,
45
+ firstSeenAt: previous.firstSeenAt || now,
46
+ lastSeenAt: now,
47
+ source,
48
+ };
49
+ await writeProjectRegistry(registry);
50
+ return registry.projects[workspace];
51
+ }
52
+
53
+ export async function discoverProjectRoots(searchRoot, options = {}) {
54
+ const root = resolve(searchRoot || process.cwd());
55
+ const maxDirs = options.maxDirs || 5000;
56
+ const roots = new Set();
57
+ let seen = 0;
58
+
59
+ async function walk(dir) {
60
+ if (seen >= maxDirs) return;
61
+ seen += 1;
62
+
63
+ if (await exists(join(dir, 'llm-wiki', '.kit-state.json'))) {
64
+ roots.add(dir);
65
+ }
66
+
67
+ let entries = [];
68
+ try {
69
+ entries = await readdir(dir, { withFileTypes: true });
70
+ } catch {
71
+ return;
72
+ }
73
+
74
+ for (const entry of entries) {
75
+ if (seen >= maxDirs) return;
76
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
77
+ await walk(join(dir, entry.name));
78
+ }
79
+ }
80
+
81
+ await walk(root);
82
+ return [...roots].sort();
83
+ }
84
+
85
+ export async function knownProjectRoots(options = {}) {
86
+ const registry = await readProjectRegistry();
87
+ const roots = new Set(Object.keys(registry.projects || {}));
88
+ if (options.discover !== false) {
89
+ const discovered = await discoverProjectRoots(options.workspace || process.cwd(), options);
90
+ for (const root of discovered) roots.add(root);
91
+ }
92
+
93
+ const existing = [];
94
+ for (const root of [...roots].sort()) {
95
+ if (await isDirectory(root) && await exists(join(root, 'llm-wiki', '.kit-state.json'))) {
96
+ existing.push(root);
97
+ }
98
+ }
99
+ return existing;
100
+ }
101
+
102
+ export function commandForProject(command, workspace) {
103
+ return `llm-wiki ${command} --workspace ${workspace}`;
104
+ }
package/src/update.js CHANGED
@@ -3,6 +3,7 @@ import { resolve } from 'path';
3
3
  import { appendWikiLog, bootstrapProject } from './project.js';
4
4
  import { install } from './install.js';
5
5
  import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
6
+ import { knownProjectRoots } from './projects.js';
6
7
  import { binPath, detectInstallSource, packageName, runtimeVersion } from './version.js';
7
8
 
8
9
  function runCommand(command, args, options = {}) {
@@ -60,6 +61,30 @@ export async function postUpdate(options = {}) {
60
61
  workspace,
61
62
  replaceHooks: true,
62
63
  });
64
+
65
+ if (options.all) {
66
+ const workspaces = await knownProjectRoots({ workspace });
67
+ const projects = [];
68
+ for (const projectRoot of workspaces) {
69
+ const projectResult = options.noProject
70
+ ? null
71
+ : await applyProjectTemplateUpdate(projectRoot);
72
+ if (!options.noProject) {
73
+ await appendWikiLog(projectRoot, `llm-wiki-kit post-update applied runtime ${runtimeVersion()}; changed templates: ${projectResult.changed.length}`);
74
+ }
75
+ projects.push({
76
+ workspace: projectRoot,
77
+ project: projectResult,
78
+ });
79
+ }
80
+ return {
81
+ workspace,
82
+ runtimeVersion: runtimeVersion(),
83
+ install: installResult,
84
+ projects,
85
+ };
86
+ }
87
+
63
88
  const projectResult = options.noProject
64
89
  ? null
65
90
  : await applyProjectTemplateUpdate(workspace);
@@ -110,6 +135,7 @@ export async function update(options = {}) {
110
135
  assertCommandOk(installResult, 'npm install -g');
111
136
 
112
137
  const postArgs = [binCommand(), 'post-update', '--workspace', workspace];
138
+ if (options.all) postArgs.push('--all');
113
139
  if (options.profile) postArgs.push('--profile', options.profile);
114
140
  if (options.noProject) postArgs.push('--no-project');
115
141
  if (options.codex === false) postArgs.push('--no-codex');