@indigoai-us/hq-cloud 6.14.2 → 6.14.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/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +46 -2
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +55 -0
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/personal-vault.d.ts +29 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +48 -2
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +44 -1
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +15 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +19 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/reindex.test.ts +69 -0
- package/src/cli/reindex.ts +49 -2
- package/src/index.ts +2 -0
- package/src/personal-vault.test.ts +57 -0
- package/src/personal-vault.ts +48 -2
- package/src/watcher.test.ts +30 -0
- package/src/watcher.ts +17 -0
package/src/cli/reindex.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* hq reindex — surfaces namespaced skills as Claude Code skill wrappers under
|
|
3
3
|
* .claude/skills/<ns>:<skill>/ (one symlink per source file), mirrors
|
|
4
4
|
* personal/{knowledge,policies,workers,settings}/<entry> into core/<type>/,
|
|
5
|
-
* prunes orphan wrappers + legacy command symlinks,
|
|
6
|
-
* workers
|
|
5
|
+
* prunes orphan wrappers + legacy command symlinks, captures this root's Claude
|
|
6
|
+
* Code session logs into workspace/.session-logs, and regenerates the workers
|
|
7
|
+
* registry.
|
|
7
8
|
*
|
|
8
9
|
* The logic historically lived in a bundled bash script (scripts/reindex.sh)
|
|
9
10
|
* that this module exec'd via `bash`. It is now implemented natively in
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
*/
|
|
17
18
|
import { spawnSync } from "child_process";
|
|
18
19
|
import * as fs from "fs";
|
|
20
|
+
import * as os from "os";
|
|
19
21
|
import * as path from "path";
|
|
20
22
|
import {
|
|
21
23
|
acquireOperationLock,
|
|
@@ -197,6 +199,46 @@ function safeSymlink(target: string, linkPath: string, label: string): boolean {
|
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
// Copy every file/folder under Claude Code's session-log directory for this HQ
|
|
203
|
+
// root (`<claude-config>/projects/<encoded-root>`) into
|
|
204
|
+
// `workspace/.session-logs`, so the raw session transcripts are captured inside
|
|
205
|
+
// the HQ tree (where sync/search can see them) on each reindex.
|
|
206
|
+
//
|
|
207
|
+
// Contract (per the feature request):
|
|
208
|
+
// - copies ALL files and folders from the projects dir into the dest,
|
|
209
|
+
// - OVERWRITES existing dest files (force), and
|
|
210
|
+
// - NEVER deletes dest entries that lack a source counterpart — this is a
|
|
211
|
+
// one-way overlay, not a mirror, so anything already parked in
|
|
212
|
+
// workspace/.session-logs survives even after Claude Code rotates or prunes
|
|
213
|
+
// its own projects dir.
|
|
214
|
+
//
|
|
215
|
+
// Best-effort: any failure warns to stderr and is swallowed so it can never
|
|
216
|
+
// fail the reindex. `<claude-config>` honors CLAUDE_CONFIG_DIR, else ~/.claude.
|
|
217
|
+
function copySessionLogs(root: string): void {
|
|
218
|
+
// Claude Code names a project's log dir by replacing every non-alphanumeric
|
|
219
|
+
// character in the project's cwd with '-' (e.g. /home/ec2-user/hq ->
|
|
220
|
+
// -home-ec2-user-hq). Re-derive that slug so we target THIS root's logs.
|
|
221
|
+
const slug = root.replace(/[^a-zA-Z0-9]/g, "-");
|
|
222
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
223
|
+
const src = path.join(claudeConfigDir, "projects", slug);
|
|
224
|
+
if (!isDir(src)) return; // no session logs for this root yet — nothing to do
|
|
225
|
+
|
|
226
|
+
const dest = path.join(root, "workspace", ".session-logs");
|
|
227
|
+
if (!safeMkdir(dest, "workspace/.session-logs directory")) return;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
// recursive: copy the whole tree; force: overwrite existing dest files.
|
|
231
|
+
// cpSync copies src INTO dest and never removes dest-only entries — exactly
|
|
232
|
+
// the "overwrite, but don't delete extras" contract we want.
|
|
233
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
234
|
+
} catch (err) {
|
|
235
|
+
warn(
|
|
236
|
+
`reindex: could not copy session logs from '${src}' to '${dest}' ` +
|
|
237
|
+
`(${(err as NodeJS.ErrnoException).code ?? "error"}: ${(err as Error).message}); skipping.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
// --- legacy `.claude/commands/<ns>/<skill>.md` symlink matcher --------------
|
|
201
243
|
// Mirrors the bash `case` patterns; `*` matches any chars (including `/`).
|
|
202
244
|
function isLegacyCommandTarget(t: string): boolean {
|
|
@@ -549,6 +591,11 @@ export function reindex(opts: ReindexOptions = {}): ReindexResult {
|
|
|
549
591
|
}
|
|
550
592
|
}
|
|
551
593
|
|
|
594
|
+
// --- Session-log capture --------------------------------------------------
|
|
595
|
+
// Overlay this root's Claude Code session logs into workspace/.session-logs
|
|
596
|
+
// (copy-all, overwrite, never-prune). Best-effort; never fails the reindex.
|
|
597
|
+
copySessionLogs(root);
|
|
598
|
+
|
|
552
599
|
// --- Workers registry regeneration ----------------------------------------
|
|
553
600
|
// Source of truth: each worker.yaml. The registry is a derived index — the
|
|
554
601
|
// generator (when present in the operated-on tree) keeps it in sync.
|
package/src/index.ts
CHANGED
|
@@ -164,9 +164,11 @@ export {
|
|
|
164
164
|
PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
|
|
165
165
|
PERSONAL_VAULT_COMPANY_EXCLUDED_SLUGS,
|
|
166
166
|
CONTINUITY_POINTER_REL,
|
|
167
|
+
SESSION_LOGS_SYNC_REL,
|
|
167
168
|
computePersonalVaultPaths,
|
|
168
169
|
computePersonalCompanySubdirs,
|
|
169
170
|
computeContinuityPointerPaths,
|
|
171
|
+
computeSessionLogsSyncPaths,
|
|
170
172
|
} from "./personal-vault.js";
|
|
171
173
|
export type { PersonalVaultOptions } from "./personal-vault.js";
|
|
172
174
|
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
CONTINUITY_POINTER_REL,
|
|
26
26
|
computeAgencySyncPaths,
|
|
27
27
|
AGENCY_SYNC_REL,
|
|
28
|
+
computeSessionLogsSyncPaths,
|
|
29
|
+
SESSION_LOGS_SYNC_REL,
|
|
28
30
|
PERSONAL_VAULT_COMPANY_EXCLUDED_SLUGS,
|
|
29
31
|
PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
|
|
30
32
|
} from "./personal-vault.js";
|
|
@@ -628,3 +630,58 @@ describe("personal-vault: agency-workspace carve-out", () => {
|
|
|
628
630
|
);
|
|
629
631
|
});
|
|
630
632
|
});
|
|
633
|
+
|
|
634
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
635
|
+
// Session-logs carve-out (workspace/.session-logs) — reindex-captured Claude
|
|
636
|
+
// Code transcripts pierce the workspace/ exclusion and round-trip to the vault.
|
|
637
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
638
|
+
describe("personal-vault: session-logs carve-out", () => {
|
|
639
|
+
let hqRoot: string;
|
|
640
|
+
beforeEach(() => {
|
|
641
|
+
hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-sesslog-test-"));
|
|
642
|
+
});
|
|
643
|
+
afterEach(() => {
|
|
644
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
645
|
+
});
|
|
646
|
+
const relAll = (abs: string[]): string[] =>
|
|
647
|
+
abs.map((p) => path.relative(hqRoot, p)).sort();
|
|
648
|
+
|
|
649
|
+
it("constant: SESSION_LOGS_SYNC_REL is workspace/.session-logs", () => {
|
|
650
|
+
expect(SESSION_LOGS_SYNC_REL).toBe("workspace/.session-logs");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("includes workspace/.session-logs when it exists", () => {
|
|
654
|
+
fs.mkdirSync(path.join(hqRoot, "workspace", ".session-logs", "tool-results"), {
|
|
655
|
+
recursive: true,
|
|
656
|
+
});
|
|
657
|
+
expect(relAll(computeSessionLogsSyncPaths(hqRoot))).toEqual([
|
|
658
|
+
path.join("workspace", ".session-logs"),
|
|
659
|
+
]);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("empty when workspace/.session-logs is absent (fail-soft)", () => {
|
|
663
|
+
fs.mkdirSync(path.join(hqRoot, "workspace"), { recursive: true });
|
|
664
|
+
expect(computeSessionLogsSyncPaths(hqRoot)).toEqual([]);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("composes into computePersonalVaultPaths without broadening workspace/", () => {
|
|
668
|
+
fs.mkdirSync(path.join(hqRoot, "workspace", ".session-logs"), { recursive: true });
|
|
669
|
+
fs.writeFileSync(
|
|
670
|
+
path.join(hqRoot, "workspace", ".session-logs", "session.jsonl"),
|
|
671
|
+
"l\n",
|
|
672
|
+
);
|
|
673
|
+
// A sibling under workspace/ that must STAY local.
|
|
674
|
+
fs.mkdirSync(path.join(hqRoot, "workspace", "locks"), { recursive: true });
|
|
675
|
+
fs.writeFileSync(path.join(hqRoot, "workspace", "locks", "x.lock"), "1");
|
|
676
|
+
fs.mkdirSync(path.join(hqRoot, ".claude"), { recursive: true });
|
|
677
|
+
|
|
678
|
+
const out = relAll(computePersonalVaultPaths(hqRoot));
|
|
679
|
+
expect(out).toContain(path.join("workspace", ".session-logs"));
|
|
680
|
+
expect(out).toContain(".claude");
|
|
681
|
+
// The carve-out is exactly the .session-logs subtree — no other workspace/
|
|
682
|
+
// sibling is pulled in.
|
|
683
|
+
expect(out.some((p) => p.startsWith(path.join("workspace", "locks")))).toBe(
|
|
684
|
+
false,
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
});
|
package/src/personal-vault.ts
CHANGED
|
@@ -26,7 +26,12 @@
|
|
|
26
26
|
* the routing source-of-truth.)
|
|
27
27
|
* - `repos/`, `workspace/`: per user directive — heavy local-only
|
|
28
28
|
* content (cloned remotes, session threads) that has no business in
|
|
29
|
-
* the personal vault.
|
|
29
|
+
* the personal vault. Three explicit sub-paths pierce this exclusion and
|
|
30
|
+
* DO round-trip: `workspace/threads/handoff.json` (+ its active thread) via
|
|
31
|
+
* {@link computeContinuityPointerPaths}, `workspace/agency/` via
|
|
32
|
+
* {@link computeAgencySyncPaths}, and `workspace/.session-logs/` (the
|
|
33
|
+
* reindex-captured Claude Code transcripts) via
|
|
34
|
+
* {@link computeSessionLogsSyncPaths}.
|
|
30
35
|
*
|
|
31
36
|
* Note: `core/`, `data/`, and `personal/` were previously excluded but are
|
|
32
37
|
* INCLUDED as of user directive 2026-05-13. `core/` ships the hq-core
|
|
@@ -134,7 +139,15 @@ export function computePersonalVaultPaths(
|
|
|
134
139
|
);
|
|
135
140
|
const continuity = computeContinuityPointerPaths(hqRoot);
|
|
136
141
|
const agency = computeAgencySyncPaths(hqRoot);
|
|
137
|
-
|
|
142
|
+
const sessionLogs = computeSessionLogsSyncPaths(hqRoot);
|
|
143
|
+
return [
|
|
144
|
+
...topLevel,
|
|
145
|
+
...manifest,
|
|
146
|
+
...companySubdirs,
|
|
147
|
+
...continuity,
|
|
148
|
+
...agency,
|
|
149
|
+
...sessionLogs,
|
|
150
|
+
];
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
/**
|
|
@@ -261,6 +274,39 @@ export function computeAgencySyncPaths(hqRoot: string): string[] {
|
|
|
261
274
|
return [];
|
|
262
275
|
}
|
|
263
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Fixed relative path (forward-slash, hq-root-relative) of the session-logs
|
|
279
|
+
* subtree. See {@link computeSessionLogsSyncPaths}.
|
|
280
|
+
*/
|
|
281
|
+
export const SESSION_LOGS_SYNC_REL = "workspace/.session-logs";
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Compute the absolute path of the session-logs carve-out:
|
|
285
|
+
* `workspace/.session-logs/` (the whole subtree).
|
|
286
|
+
*
|
|
287
|
+
* `hq reindex` copies this HQ root's Claude Code session transcripts here (see
|
|
288
|
+
* reindex's copySessionLogs). Like the agency carve-out above, this pierces the
|
|
289
|
+
* `workspace/` top-level exclusion for ONE specific subtree so those raw
|
|
290
|
+
* transcripts round-trip to the personal vault / S3 — the operator's explicit
|
|
291
|
+
* directive is that the session logs travel with the vault. The rest of
|
|
292
|
+
* `workspace/` (session scratch, locks, reports, worktrees) stays machine-local.
|
|
293
|
+
*
|
|
294
|
+
* Returns the directory path when it exists; share()'s per-file walk handles
|
|
295
|
+
* recursion, and the nested personal-vault exclusions still apply to files
|
|
296
|
+
* inside it. Fail-soft: a missing or unreadable `workspace/.session-logs/`
|
|
297
|
+
* returns []. Callers tolerate empty arrays — same contract as the manifest,
|
|
298
|
+
* continuity, and agency special-cases.
|
|
299
|
+
*/
|
|
300
|
+
export function computeSessionLogsSyncPaths(hqRoot: string): string[] {
|
|
301
|
+
const sessionLogsDir = path.join(hqRoot, "workspace", ".session-logs");
|
|
302
|
+
try {
|
|
303
|
+
if (fs.statSync(sessionLogsDir).isDirectory()) return [sessionLogsDir];
|
|
304
|
+
} catch {
|
|
305
|
+
// No session logs captured on this machine yet — nothing to carry.
|
|
306
|
+
}
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
/**
|
|
265
311
|
* Discover `companies/{slug}/` subdirs that should sync to the personal
|
|
266
312
|
* bucket as a fallback for companies the operator has not designated as
|
package/src/watcher.test.ts
CHANGED
|
@@ -371,6 +371,36 @@ describe("US-002: createWatchPathFilter — personal-vault exclusions", () => {
|
|
|
371
371
|
expect(personal(path.join(ROOT, "workspace/worktrees/x/a.ts"))).toBe(false);
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
+
// ── workspace/.session-logs subtree carve-out (personal mode) ────────────
|
|
375
|
+
// The personal push uploads workspace/.session-logs/** (reindex-captured
|
|
376
|
+
// Claude Code transcripts, computeSessionLogsSyncPaths), so the personal
|
|
377
|
+
// watcher must emit those changes event-driven. Mirrors the agency carve-out.
|
|
378
|
+
it("DOES emit for workspace/.session-logs files in personalMode (synced subtree)", () => {
|
|
379
|
+
expect(
|
|
380
|
+
personal(path.join(ROOT, "workspace/.session-logs/session.jsonl")),
|
|
381
|
+
).toBe(true);
|
|
382
|
+
expect(
|
|
383
|
+
personal(path.join(ROOT, "workspace/.session-logs/tool-results/r.txt")),
|
|
384
|
+
).toBe(true);
|
|
385
|
+
// The subtree root itself, as a file event, also emits.
|
|
386
|
+
expect(personal(path.join(ROOT, "workspace/.session-logs"))).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("allows chokidar to DESCEND to workspace/.session-logs in personalMode (dir probes)", () => {
|
|
390
|
+
expect(personal(path.join(ROOT, "workspace"), true)).toBe(true);
|
|
391
|
+
expect(personal(path.join(ROOT, "workspace/.session-logs"), true)).toBe(true);
|
|
392
|
+
expect(
|
|
393
|
+
personal(path.join(ROOT, "workspace/.session-logs/tool-results"), true),
|
|
394
|
+
).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("session-logs carve-out is exact-segment (does NOT leak to a similarly-named sibling)", () => {
|
|
398
|
+
expect(
|
|
399
|
+
personal(path.join(ROOT, "workspace/.session-logs-old/foo.txt")),
|
|
400
|
+
).toBe(false);
|
|
401
|
+
expect(personal(path.join(ROOT, "workspace/.session-logsX.md"))).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
|
|
374
404
|
it("does not apply the carve-out in non-personal mode", () => {
|
|
375
405
|
// In non-personal mode the excluded-top-level layer never runs, so the
|
|
376
406
|
// carve-out is irrelevant; handoff.json passes via the ordinary ignore
|
package/src/watcher.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { isPersonalVaultExcluded } from "./personal-vault-exclusions.js";
|
|
|
16
16
|
import {
|
|
17
17
|
AGENCY_SYNC_REL,
|
|
18
18
|
CONTINUITY_POINTER_REL,
|
|
19
|
+
SESSION_LOGS_SYNC_REL,
|
|
19
20
|
PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
|
|
20
21
|
} from "./personal-vault.js";
|
|
21
22
|
import type { PushEvent } from "./sync/push-event.js";
|
|
@@ -466,6 +467,22 @@ export function createWatchPathFilter(
|
|
|
466
467
|
}
|
|
467
468
|
if (isDir && AGENCY_SYNC_REL.startsWith(rel + "/")) return true;
|
|
468
469
|
|
|
470
|
+
// Session-logs carve-out: `workspace/.session-logs/` (the whole subtree)
|
|
471
|
+
// IS synced via computeSessionLogsSyncPaths (mirrored on the push side),
|
|
472
|
+
// so it must re-include BEFORE the top-level `workspace/` rejection below
|
|
473
|
+
// — identical shape to the agency carve-out just above. Without this, a
|
|
474
|
+
// `--personal --watch` runner never emits reindex-captured session-log
|
|
475
|
+
// changes and they wait for the 60s poll. Paths at/under it emit; the
|
|
476
|
+
// subtree root and its ancestor dir (`workspace`) return true for the dir
|
|
477
|
+
// probe so chokidar descends to reach them instead of pruning `workspace/`.
|
|
478
|
+
if (
|
|
479
|
+
rel === SESSION_LOGS_SYNC_REL ||
|
|
480
|
+
rel.startsWith(SESSION_LOGS_SYNC_REL + "/")
|
|
481
|
+
) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
if (isDir && SESSION_LOGS_SYNC_REL.startsWith(rel + "/")) return true;
|
|
485
|
+
|
|
469
486
|
// Layer 3: excluded top-level buckets (.git/companies/repos/workspace).
|
|
470
487
|
const topLevel = rel.split("/")[0];
|
|
471
488
|
if (excludedTopLevel.has(topLevel)) return false;
|