@indigoai-us/hq-cli 5.92.0 → 5.93.1
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/CHANGELOG.md +20 -0
- package/assets/scaffold/core/scripts/archive-old-threads.sh +6 -2
- package/dist/commands/core.js +6 -12
- package/dist/lib/core-utils/archive-old-threads.d.ts +8 -0
- package/dist/lib/core-utils/archive-old-threads.js +97 -0
- package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +15 -0
- package/dist/lib/core-utils/qmd-reindex-after-sync.js +62 -0
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.93.0]
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq core archive-old-threads --gated` no longer fails on Linux. The retained
|
|
10
|
+
shell asset probed `stat -f %m` first, which on GNU coreutils is a filesystem
|
|
11
|
+
query: it consumed the format string as a path, printed `File: …` for the
|
|
12
|
+
real argument, and exited 0, so the `stat -c %Y` fallback never ran and the
|
|
13
|
+
age arithmetic aborted on an unbound `File`. The once-per-day gate exited 1
|
|
14
|
+
instead of gating. Probes GNU first now. (#327)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `hq core archive-old-threads` and `hq core qmd-reindex-after-sync` run as
|
|
19
|
+
native, tested TypeScript instead of bundled shell assets, proven equivalent
|
|
20
|
+
by a shell-vs-native differential suite. The reindex command now reuses the
|
|
21
|
+
existing search-index collection policy rather than restating the
|
|
22
|
+
registration convention, so the two can no longer drift. Scaffold scripts
|
|
23
|
+
still ship for loose HQ trees. (#327)
|
|
24
|
+
|
|
5
25
|
## [5.92.0]
|
|
6
26
|
|
|
7
27
|
### Changed
|
|
@@ -32,8 +32,12 @@ done
|
|
|
32
32
|
|
|
33
33
|
TOUCHFILE="workspace/.last-archive-run"
|
|
34
34
|
if [[ "$GATED" == "true" && -f "$TOUCHFILE" ]]; then
|
|
35
|
-
#
|
|
36
|
-
|
|
35
|
+
# GNU `stat -c %Y` first, then macOS `stat -f %m`. Order matters: on GNU,
|
|
36
|
+
# `-f` queries a FILE SYSTEM, so `stat -f %m <file>` treats `%m` as a path,
|
|
37
|
+
# prints `File: …` for the real argument, and still exits 0 — the fallback
|
|
38
|
+
# never fired and the arithmetic below died on unbound `File`, breaking
|
|
39
|
+
# --gated on Linux entirely. Matches core/scripts/lib/portable.sh.
|
|
40
|
+
mtime=$(stat -c %Y "$TOUCHFILE" 2>/dev/null || stat -f %m "$TOUCHFILE" 2>/dev/null || echo 0)
|
|
37
41
|
now=$(date +%s)
|
|
38
42
|
if [[ $((now - mtime)) -lt 86400 ]]; then
|
|
39
43
|
echo "archive-old-threads: gated (ran <24h ago, skipping)" >&2
|
package/dist/commands/core.js
CHANGED
|
@@ -42,6 +42,8 @@ import { resizeScreenshot } from "../lib/core-utils/resize-screenshot.js";
|
|
|
42
42
|
import { tokenUsageReport } from "../lib/core-utils/token-usage-report.js";
|
|
43
43
|
import { createWorktree } from "../lib/core-utils/worktree.js";
|
|
44
44
|
import { runCodexSkillBridgeCommand } from "../lib/core-utils/codex-skill-bridge-entry.js";
|
|
45
|
+
import { archiveOldThreads } from "../lib/core-utils/archive-old-threads.js";
|
|
46
|
+
import { qmdReindexAfterSync } from "../lib/core-utils/qmd-reindex-after-sync.js";
|
|
45
47
|
/**
|
|
46
48
|
* These commands are now native implementations. Their retained shell assets
|
|
47
49
|
* are deliberately claimed in SCAFFOLD_ONLY_ASSETS: loose HQ trees still ship
|
|
@@ -55,6 +57,8 @@ export const NATIVE_UTILITY_COMMANDS = [
|
|
|
55
57
|
{ name: "token-usage-report", root: "cwd", summary: "Report session token usage", run: (args) => tokenUsageReport(args) },
|
|
56
58
|
{ name: "worktree", root: "live", summary: "Create a git worktree under workspace/worktrees/", run: (args, context) => createWorktree(args, { cwd: context.cwd, hqRoot: context.hqRoot }) },
|
|
57
59
|
{ name: "codex-skill-bridge", root: "live", summary: "Install/inspect the Codex skill bridge", run: (args, context) => runCodexSkillBridgeCommand(args, { root: context.hqRoot ?? context.cwd }) },
|
|
60
|
+
{ name: "archive-old-threads", root: "live", summary: "Archive aged session-thread files", run: (args, context) => archiveOldThreads(args, { hqRoot: context.hqRoot ?? context.cwd }) },
|
|
61
|
+
{ name: "qmd-reindex-after-sync", root: "cwd", summary: "Refresh qmd collections after a sync", run: (args, context) => qmdReindexAfterSync(args, { cwd: context.cwd }) },
|
|
58
62
|
];
|
|
59
63
|
export const NATIVE_SCAFFOLD_COMMANDS = [
|
|
60
64
|
{
|
|
@@ -161,12 +165,6 @@ export const REBUILD_INDEX_TARGETS = [
|
|
|
161
165
|
*/
|
|
162
166
|
export const SCAFFOLD_COMMANDS = [
|
|
163
167
|
// ---- One-shot migrations and maintenance (live tree) ----
|
|
164
|
-
{
|
|
165
|
-
name: "archive-old-threads",
|
|
166
|
-
asset: "core/scripts/archive-old-threads.sh",
|
|
167
|
-
root: "live",
|
|
168
|
-
summary: "Archive aged session-thread files",
|
|
169
|
-
},
|
|
170
168
|
{
|
|
171
169
|
name: "backfill-company-skill-mirrors",
|
|
172
170
|
asset: "core/scripts/backfill-company-skill-mirrors.sh",
|
|
@@ -179,12 +177,6 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
179
177
|
root: "cwd",
|
|
180
178
|
summary: "One-shot: mirror threads into company workspaces",
|
|
181
179
|
},
|
|
182
|
-
{
|
|
183
|
-
name: "qmd-reindex-after-sync",
|
|
184
|
-
asset: "core/scripts/qmd-reindex-after-sync.sh",
|
|
185
|
-
root: "cwd",
|
|
186
|
-
summary: "Refresh qmd collections after a sync",
|
|
187
|
-
},
|
|
188
180
|
];
|
|
189
181
|
/**
|
|
190
182
|
* Bundled solely for HQ-root scaffolds, which still need these as loose files.
|
|
@@ -192,7 +184,9 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
192
184
|
* direct `hq core` command.
|
|
193
185
|
*/
|
|
194
186
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
187
|
+
{ asset: "core/scripts/archive-old-threads.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
195
188
|
{ asset: "core/scripts/codex-skill-bridge.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
189
|
+
{ asset: "core/scripts/qmd-reindex-after-sync.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
196
190
|
{ asset: "core/scripts/detect-stale-core-policy-mirror.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
197
191
|
{ asset: "core/scripts/hq-status-summary.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
198
192
|
{ asset: "core/scripts/ontology-readme-drift.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
export type ArchiveOptions = UtilityIo & {
|
|
3
|
+
hqRoot?: string;
|
|
4
|
+
/** Injected for tests; defaults to the wall clock. */
|
|
5
|
+
now?: () => Date;
|
|
6
|
+
};
|
|
7
|
+
export declare function archiveOldThreads(args?: string[], options?: ArchiveOptions): number;
|
|
8
|
+
//# sourceMappingURL=archive-old-threads.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native implementation of `core/scripts/archive-old-threads.sh`.
|
|
3
|
+
*
|
|
4
|
+
* Moves `workspace/threads/T-YYYYMMDD-*.json` older than the cutoff into
|
|
5
|
+
* `workspace/threads/archive/YYYY-MM/`. Idempotent: files already under
|
|
6
|
+
* `archive/` are never listed, so repeat runs are no-ops.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { ioFor, line } from "./common.js";
|
|
11
|
+
/** `date -v-Nd +%Y%m%d` / `date -d "-N days" +%Y%m%d`, in local time. */
|
|
12
|
+
function cutoffStamp(now, days) {
|
|
13
|
+
const cutoff = new Date(now.getTime());
|
|
14
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
15
|
+
const year = cutoff.getFullYear();
|
|
16
|
+
const month = `${cutoff.getMonth() + 1}`.padStart(2, "0");
|
|
17
|
+
const day = `${cutoff.getDate()}`.padStart(2, "0");
|
|
18
|
+
return `${year}${month}${day}`;
|
|
19
|
+
}
|
|
20
|
+
export function archiveOldThreads(args = [], options = {}) {
|
|
21
|
+
const { stderr } = ioFor(options);
|
|
22
|
+
const hqRoot = options.hqRoot ?? process.env.HQ_ROOT ?? process.cwd();
|
|
23
|
+
const now = options.now ?? (() => new Date());
|
|
24
|
+
let cutoffDays = 60;
|
|
25
|
+
let dryRun = false;
|
|
26
|
+
let gated = false;
|
|
27
|
+
for (let index = 0; index < args.length; index++) {
|
|
28
|
+
const arg = args[index];
|
|
29
|
+
if (arg === "--days") {
|
|
30
|
+
cutoffDays = Number(args[++index]);
|
|
31
|
+
}
|
|
32
|
+
else if (arg === "--dry-run") {
|
|
33
|
+
dryRun = true;
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--gated") {
|
|
36
|
+
gated = true;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
line(stderr, `Unknown arg: ${arg}`);
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const touchfile = path.join(hqRoot, "workspace/.last-archive-run");
|
|
44
|
+
if (gated && fs.existsSync(touchfile)) {
|
|
45
|
+
const mtime = Math.floor(fs.statSync(touchfile).mtimeMs / 1000);
|
|
46
|
+
const seconds = Math.floor(now().getTime() / 1000);
|
|
47
|
+
if (seconds - mtime < 86_400) {
|
|
48
|
+
line(stderr, "archive-old-threads: gated (ran <24h ago, skipping)");
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const threadsDir = path.join(hqRoot, "workspace/threads");
|
|
53
|
+
const archiveRoot = path.join(threadsDir, "archive");
|
|
54
|
+
fs.mkdirSync(archiveRoot, { recursive: true });
|
|
55
|
+
const cutoff = cutoffStamp(now(), cutoffDays);
|
|
56
|
+
// `ls workspace/threads/T-*.json`: top level only, name-ordered, archive/ excluded.
|
|
57
|
+
let names;
|
|
58
|
+
try {
|
|
59
|
+
names = fs
|
|
60
|
+
.readdirSync(threadsDir)
|
|
61
|
+
.filter((name) => name.startsWith("T-") && name.endsWith(".json"))
|
|
62
|
+
.filter((name) => fs.lstatSync(path.join(threadsDir, name)).isFile())
|
|
63
|
+
.sort();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
names = [];
|
|
67
|
+
}
|
|
68
|
+
let moved = 0;
|
|
69
|
+
let skipped = 0;
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const match = name.match(/^T-(\d{8})-/);
|
|
72
|
+
if (!match) {
|
|
73
|
+
skipped += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const datePart = match[1];
|
|
77
|
+
// Both stamps are 8-digit YYYYMMDD, so numeric and lexical order agree.
|
|
78
|
+
if (Number(datePart) >= Number(cutoff))
|
|
79
|
+
continue;
|
|
80
|
+
const bucket = path.join(archiveRoot, `${datePart.slice(0, 4)}-${datePart.slice(4, 6)}`);
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
line(stderr, `WOULD MOVE: ${path.join("workspace/threads", name)} -> ${path.relative(hqRoot, bucket)}/`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
fs.mkdirSync(bucket, { recursive: true });
|
|
86
|
+
fs.renameSync(path.join(threadsDir, name), path.join(bucket, name));
|
|
87
|
+
}
|
|
88
|
+
moved += 1;
|
|
89
|
+
}
|
|
90
|
+
if (!dryRun && gated) {
|
|
91
|
+
fs.mkdirSync(path.dirname(touchfile), { recursive: true });
|
|
92
|
+
fs.writeFileSync(touchfile, "");
|
|
93
|
+
}
|
|
94
|
+
line(stderr, `archive-old-threads: cutoff=${cutoffDays}d (before ${cutoff}) moved=${moved} skipped=${skipped} dry_run=${dryRun}`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=archive-old-threads.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UtilityIo } from "./common.js";
|
|
2
|
+
export type QmdReindexOptions = UtilityIo & {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
/** Test seams; default to the real search-index implementations. */
|
|
5
|
+
resolveBin?: () => string;
|
|
6
|
+
reconcile?: (hqRoot: string, options: {
|
|
7
|
+
bin: string;
|
|
8
|
+
}) => unknown;
|
|
9
|
+
run?: (args: string[], options: {
|
|
10
|
+
bin: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
}) => unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare function qmdReindexAfterSync(args?: string[], options?: QmdReindexOptions): number;
|
|
15
|
+
//# sourceMappingURL=qmd-reindex-after-sync.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native implementation of `core/scripts/qmd-reindex-after-sync.sh`.
|
|
3
|
+
*
|
|
4
|
+
* The shell script restated the collection-registration convention that
|
|
5
|
+
* `src/lib/search-index` already owns; this port reuses that single source
|
|
6
|
+
* instead, so the two can no longer drift.
|
|
7
|
+
*
|
|
8
|
+
* Contract preserved verbatim: a missing qmd or a non-HQ path is a silent
|
|
9
|
+
* success (a sync must never fail because indexing could not run), the lexical
|
|
10
|
+
* update always runs, and embeddings rebuild only on `--embed`.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { reconcileCollections, resolveQmdBin, runQmd } from "../search-index/index.js";
|
|
15
|
+
export function qmdReindexAfterSync(args = [], options = {}) {
|
|
16
|
+
let hqRoot = "";
|
|
17
|
+
let embed = false;
|
|
18
|
+
for (const arg of args) {
|
|
19
|
+
if (arg === "--embed")
|
|
20
|
+
embed = true;
|
|
21
|
+
else if (arg.startsWith("--"))
|
|
22
|
+
continue; // unknown flags are ignored, as in the shell
|
|
23
|
+
else if (!hqRoot)
|
|
24
|
+
hqRoot = arg;
|
|
25
|
+
}
|
|
26
|
+
if (!hqRoot)
|
|
27
|
+
hqRoot = options.cwd ?? process.cwd();
|
|
28
|
+
let bin;
|
|
29
|
+
try {
|
|
30
|
+
bin = (options.resolveBin ?? resolveQmdBin)();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 0; // qmd absent: silent no-op.
|
|
34
|
+
}
|
|
35
|
+
if (!fs.existsSync(path.join(hqRoot, "core/core.yaml")))
|
|
36
|
+
return 0;
|
|
37
|
+
const reconcile = options.reconcile ?? ((root, opts) => reconcileCollections(root, opts));
|
|
38
|
+
const run = options.run ?? ((argv, opts) => runQmd(argv, opts));
|
|
39
|
+
// Every qmd interaction is best-effort: the shell suffixed each with `|| true`.
|
|
40
|
+
try {
|
|
41
|
+
reconcile(hqRoot, { bin });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* registration is advisory; an update still helps */
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
run(["update"], { bin, cwd: hqRoot });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* never fail a sync on index freshness */
|
|
51
|
+
}
|
|
52
|
+
if (embed) {
|
|
53
|
+
try {
|
|
54
|
+
run(["embed"], { bin, cwd: hqRoot });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* embeddings are deferred-cost and optional */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=qmd-reindex-after-sync.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.93.1",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -73,9 +73,15 @@
|
|
|
73
73
|
"engines": {
|
|
74
74
|
"node": ">=20.0.0"
|
|
75
75
|
},
|
|
76
|
+
"overrides": {
|
|
77
|
+
"better-sqlite3": "^12.11.1"
|
|
78
|
+
},
|
|
76
79
|
"pnpm": {
|
|
77
80
|
"onlyBuiltDependencies": [
|
|
78
81
|
"better-sqlite3"
|
|
79
|
-
]
|
|
82
|
+
],
|
|
83
|
+
"overrides": {
|
|
84
|
+
"better-sqlite3": "^12.11.1"
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
}
|