@indigoai-us/hq-cli 5.316.0 → 5.317.0

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.
@@ -0,0 +1,327 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { resolveSessionHelperRuntime } from "./core-session-helper-runtime.js";
4
+ const USAGE = `session-journal.sh: usage:
5
+ session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
6
+ session-journal.sh list [--date YYYY-MM-DD]
7
+ session-journal.sh read <NNN> [--date YYYY-MM-DD]
8
+ session-journal.sh index-path [--date YYYY-MM-DD]
9
+ session-journal.sh dir-path [--date YYYY-MM-DD]
10
+ session-journal.sh tool-counter (increment|read|reset) [--session KEY]
11
+ `;
12
+ const counterKey = (value) => {
13
+ let key = [...Buffer.from(value, "utf8")]
14
+ .map((byte) => (byte >= 0x41 && byte <= 0x5a)
15
+ || (byte >= 0x61 && byte <= 0x7a)
16
+ || (byte >= 0x30 && byte <= 0x39)
17
+ || byte === 0x2e || byte === 0x5f || byte === 0x2d
18
+ ? String.fromCharCode(byte) : "_")
19
+ .join("")
20
+ .replace(/^_/, "")
21
+ .replace(/_$/, "");
22
+ if (!key)
23
+ key = "unscoped";
24
+ return key.length > 96 ? key.slice(0, 96) : key;
25
+ };
26
+ const slugify = (value) => value.toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40);
28
+ const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
29
+ const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
30
+ const hasFile = (file) => {
31
+ try {
32
+ return fs.statSync(file).isFile();
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ };
38
+ const readCounter = (file) => {
39
+ let raw = "";
40
+ try {
41
+ if (hasFile(file))
42
+ raw = fs.readFileSync(file, "utf8").replace(/[^0-9]/g, "");
43
+ }
44
+ catch { /* grep/tr failure is empty */ }
45
+ return Number.parseInt(raw || "0", 10) || 0;
46
+ };
47
+ const sleepSync = (ms) => {
48
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
49
+ };
50
+ function processIsAlive(pid) {
51
+ try {
52
+ process.kill(pid, 0);
53
+ return true;
54
+ }
55
+ catch (error) {
56
+ return error.code !== "ESRCH";
57
+ }
58
+ }
59
+ function acquireJournalWriteLock(dir) {
60
+ const lock = path.join(dir, ".session-journal-write.lock");
61
+ const ownerFile = path.join(lock, "pid");
62
+ const deadline = Date.now() + 5_000;
63
+ while (Date.now() < deadline) {
64
+ try {
65
+ fs.mkdirSync(lock);
66
+ try {
67
+ fs.writeFileSync(ownerFile, `${process.pid}\n`, { flag: "wx" });
68
+ }
69
+ catch (error) {
70
+ fs.rmSync(lock, { recursive: true, force: true });
71
+ return undefined;
72
+ }
73
+ return lock;
74
+ }
75
+ catch (error) {
76
+ if (error.code !== "EEXIST")
77
+ return undefined;
78
+ let owner = 0;
79
+ try {
80
+ owner = Number.parseInt(fs.readFileSync(ownerFile, "utf8"), 10) || 0;
81
+ }
82
+ catch { /* new owner has not written its pid yet */ }
83
+ if (owner > 0 && !processIsAlive(owner)) {
84
+ let confirmedOwner = 0;
85
+ try {
86
+ confirmedOwner = Number.parseInt(fs.readFileSync(ownerFile, "utf8"), 10) || 0;
87
+ }
88
+ catch { /* lock disappeared */ }
89
+ if (confirmedOwner === owner) {
90
+ const stale = `${lock}.stale-${process.pid}-${Math.random().toString(16).slice(2)}`;
91
+ try {
92
+ fs.renameSync(lock, stale);
93
+ fs.rmSync(stale, { recursive: true, force: true });
94
+ }
95
+ catch { /* another waiter reclaimed it */ }
96
+ continue;
97
+ }
98
+ }
99
+ sleepSync(20);
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+ function releaseJournalWriteLock(lock) {
105
+ try {
106
+ const owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8"), 10);
107
+ if (owner === process.pid)
108
+ fs.rmSync(lock, { recursive: true, force: true });
109
+ }
110
+ catch { /* shell lock cleanup is best-effort */ }
111
+ }
112
+ function withCounterLock(lock, action) {
113
+ let i = 0;
114
+ let acquired = false;
115
+ while (!acquired) {
116
+ try {
117
+ fs.mkdirSync(lock);
118
+ acquired = true;
119
+ }
120
+ catch {
121
+ i++;
122
+ if (i >= 50)
123
+ break;
124
+ sleepSync(20);
125
+ }
126
+ }
127
+ action();
128
+ try {
129
+ fs.rmdirSync(lock);
130
+ }
131
+ catch { /* shell rmdir is best-effort */ }
132
+ }
133
+ function nextSequence(dir) {
134
+ let names = [];
135
+ try {
136
+ names = fs.readdirSync(dir);
137
+ }
138
+ catch { /* ls failure becomes no matches */ }
139
+ const sequences = names.filter((name) => /^\d{3}-/.test(name))
140
+ .map((name) => Number(name.match(/^(\d+)-/)?.[1] ?? 0))
141
+ .filter((value) => Number.isFinite(value));
142
+ const high = sequences.length ? Math.max(...sequences) + 1 : 1;
143
+ return String(high).padStart(3, "0");
144
+ }
145
+ function options(args, valueFlags) {
146
+ const values = new Map();
147
+ for (let i = 0; i < args.length; i++) {
148
+ if (valueFlags.includes(args[i]))
149
+ values.set(args[i], args[++i] ?? "");
150
+ }
151
+ return values;
152
+ }
153
+ function runToolCounter(root, action, sessionArg, env, now) {
154
+ const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
155
+ const key = counterKey(rawKey);
156
+ const dir = journalDir(root, now().toISOString().slice(0, 10));
157
+ try {
158
+ fs.mkdirSync(dir, { recursive: true });
159
+ }
160
+ catch { /* legacy mkdir failure is ignored */ }
161
+ const counter = path.join(dir, `.tool-count-${key}`);
162
+ const lock = `${counter}.lock`;
163
+ if (action === "increment") {
164
+ withCounterLock(lock, () => {
165
+ try {
166
+ fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
167
+ }
168
+ catch { /* fail soft */ }
169
+ });
170
+ }
171
+ else if (action === "read") {
172
+ process.stdout.write(`${readCounter(counter)}\n`);
173
+ }
174
+ else if (action === "reset") {
175
+ withCounterLock(lock, () => {
176
+ try {
177
+ fs.writeFileSync(counter, "0\n");
178
+ }
179
+ catch { /* fail soft */ }
180
+ });
181
+ }
182
+ else {
183
+ warning("tool-counter: subcommand required (increment|read|reset)");
184
+ }
185
+ }
186
+ /** Native implementation of the fail-soft session journal helper. */
187
+ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies = {}) {
188
+ const runtime = resolveSessionHelperRuntime(dependencies);
189
+ const today = () => runtime.now().toISOString().slice(0, 10);
190
+ const nowIso = () => runtime.now().toISOString().replace(/\.\d{3}Z$/, "Z");
191
+ const nowHhmm = () => runtime.now().toISOString().slice(11, 16) + "Z";
192
+ const root = hqRoot ?? cwd;
193
+ const [command = "", ...rest] = args;
194
+ if (command === "write") {
195
+ const title = rest[0] ?? "";
196
+ const flags = options(rest.slice(1), ["--files", "--body-file", "--session"]);
197
+ const files = flags.get("--files") ?? "";
198
+ const bodyFile = flags.get("--body-file") ?? "";
199
+ const session = flags.get("--session") ?? "";
200
+ if (!title) {
201
+ warning("write: title required");
202
+ return 0;
203
+ }
204
+ const date = today();
205
+ const dir = journalDir(root, date);
206
+ try {
207
+ fs.mkdirSync(dir, { recursive: true });
208
+ }
209
+ catch {
210
+ warning(`write: mkdir failed: ${dir}`);
211
+ return 0;
212
+ }
213
+ const slug = slugify(title) || "entry";
214
+ const body = bodyFile && hasFile(bodyFile)
215
+ ? fs.readFileSync(bodyFile, "utf8")
216
+ : "## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n";
217
+ const writeLock = acquireJournalWriteLock(dir);
218
+ if (!writeLock) {
219
+ warning("write: failed to acquire journal lock");
220
+ return 0;
221
+ }
222
+ let seq = "";
223
+ let output = "";
224
+ let created = false;
225
+ try {
226
+ for (let attempt = 0; attempt < 10_000; attempt += 1) {
227
+ seq = nextSequence(dir);
228
+ output = path.join(dir, `${seq}-${slug}.md`);
229
+ const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${title}\n`, `slug: ${slug}\n`];
230
+ if (files) {
231
+ lines.push("files:\n");
232
+ for (const file of files.split(","))
233
+ if (file)
234
+ lines.push(` - ${file}\n`);
235
+ }
236
+ lines.push("status: closed\n", "---\n\n", `# ${seq} — ${title}\n\n`);
237
+ try {
238
+ fs.writeFileSync(output, lines.join("") + body, { flag: "wx" });
239
+ created = true;
240
+ break;
241
+ }
242
+ catch (error) {
243
+ if (error.code === "EEXIST")
244
+ continue;
245
+ warning(`write: failed to write ${output}`);
246
+ return 0;
247
+ }
248
+ }
249
+ if (!created) {
250
+ warning(`write: failed to write ${output}`);
251
+ return 0;
252
+ }
253
+ const index = path.join(dir, "INDEX.md");
254
+ if (!hasFile(index)) {
255
+ try {
256
+ fs.writeFileSync(index, `# Session journal — ${date}\n\n`);
257
+ }
258
+ catch { /* shell ignores initial failure */ }
259
+ }
260
+ try {
261
+ fs.appendFileSync(index, `- \`${seq}\` ${nowHhmm()} — ${title}\n`);
262
+ }
263
+ catch {
264
+ warning("write: INDEX update failed");
265
+ }
266
+ }
267
+ finally {
268
+ releaseJournalWriteLock(writeLock);
269
+ }
270
+ runToolCounter(root, "reset", counterKey(session || runtime.env.HQ_JOURNAL_SESSION || runtime.env.HQ_HOOK_SESSION_ID || ""), runtime.env, runtime.now);
271
+ process.stdout.write(`${output}\n`);
272
+ return 0;
273
+ }
274
+ if (command === "list" || command === "read" || command === "index-path" || command === "dir-path") {
275
+ const sequence = command === "read" ? rest[0] ?? "" : "";
276
+ const afterSequence = command === "read" ? rest.slice(1) : rest;
277
+ const date = options(afterSequence, ["--date"]).get("--date") || today();
278
+ const dir = journalDir(root, date);
279
+ if (command === "dir-path") {
280
+ process.stdout.write(dir);
281
+ return 0;
282
+ }
283
+ if (command === "index-path") {
284
+ process.stdout.write(`${dir}/INDEX.md\n`);
285
+ return 0;
286
+ }
287
+ if (command === "list") {
288
+ const index = path.join(dir, "INDEX.md");
289
+ if (hasFile(index))
290
+ process.stdout.write(fs.readFileSync(index));
291
+ else
292
+ process.stdout.write(`(no journal for ${date})\n`);
293
+ return 0;
294
+ }
295
+ if (!sequence) {
296
+ warning("read: NNN required");
297
+ return 0;
298
+ }
299
+ if (!/^[0-9]{1,18}$/.test(sequence)) {
300
+ warning("read: NNN required");
301
+ return 0;
302
+ }
303
+ const normalized = BigInt(sequence).toString().padStart(3, "0");
304
+ const match = (() => {
305
+ try {
306
+ return fs.readdirSync(dir).find((name) => name.startsWith(`${normalized}-`));
307
+ }
308
+ catch {
309
+ return undefined;
310
+ }
311
+ })();
312
+ if (match)
313
+ process.stdout.write(fs.readFileSync(path.join(dir, match)));
314
+ else
315
+ warning(`read: no entry ${normalized} for ${date}`);
316
+ return 0;
317
+ }
318
+ if (command === "tool-counter") {
319
+ const action = rest[0] ?? "";
320
+ const session = options(rest.slice(1), ["--session"]).get("--session") ?? "";
321
+ runToolCounter(root, action, session, runtime.env, runtime.now);
322
+ return 0;
323
+ }
324
+ process.stderr.write(USAGE);
325
+ return 1;
326
+ }
327
+ //# sourceMappingURL=core-session-journal-native.js.map
@@ -0,0 +1,3 @@
1
+ import { type SessionHelperRuntimeOverrides } from "./core-session-helper-runtime.js";
2
+ export declare function runSessionProjectFast(argv: readonly string[], stdin: string, cwd: string, root: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
3
+ //# sourceMappingURL=core-session-project-native.d.ts.map