hippo-memory 1.43.2 → 1.44.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.
- package/README.md +2 -2
- package/dist/audit-prune.js +4 -1
- package/dist/audit-prune.js.map +1 -1
- package/dist/capture.d.ts +11 -10
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +22 -30
- package/dist/capture.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +158 -99
- package/dist/cli.js.map +1 -1
- package/dist/db.js +4 -1
- package/dist/db.js.map +1 -1
- package/dist/embedding-provider.d.ts +1 -1
- package/dist/embedding-provider.js +1 -1
- package/dist/goals.js +12 -3
- package/dist/goals.js.map +1 -1
- package/dist/physics-state.js +4 -1
- package/dist/physics-state.js.map +1 -1
- package/dist/raw-archive.d.ts.map +1 -1
- package/dist/raw-archive.js +5 -2
- package/dist/raw-archive.js.map +1 -1
- package/dist/recall-trace.js +4 -1
- package/dist/recall-trace.js.map +1 -1
- package/dist/src/audit-prune.js +4 -1
- package/dist/src/audit-prune.js.map +1 -1
- package/dist/src/capture.js +22 -30
- package/dist/src/capture.js.map +1 -1
- package/dist/src/cli.js +158 -99
- package/dist/src/cli.js.map +1 -1
- package/dist/src/db.js +4 -1
- package/dist/src/db.js.map +1 -1
- package/dist/src/embedding-provider.js +1 -1
- package/dist/src/goals.js +12 -3
- package/dist/src/goals.js.map +1 -1
- package/dist/src/physics-state.js +4 -1
- package/dist/src/physics-state.js.map +1 -1
- package/dist/src/raw-archive.js +5 -2
- package/dist/src/raw-archive.js.map +1 -1
- package/dist/src/recall-trace.js +4 -1
- package/dist/src/recall-trace.js.map +1 -1
- package/dist/src/stdin.js +41 -0
- package/dist/src/stdin.js.map +1 -0
- package/dist/src/store.js +8 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/stdin.d.ts +12 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +41 -0
- package/dist/stdin.js.map +1 -0
- package/dist/store.js +8 -2
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -65,7 +65,8 @@ import { detectScope } from './scope.js';
|
|
|
65
65
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
66
66
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
67
67
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
|
|
68
|
-
import { cmdCapture, cmdPreCompact, truncateCodePointSafe, sanitizeLogMessage } from './capture.js';
|
|
68
|
+
import { cmdCapture, cmdPreCompact, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage } from './capture.js';
|
|
69
|
+
import { readStdinBounded } from './stdin.js';
|
|
69
70
|
import { auditMemories, appendAuditEvent, } from './audit.js';
|
|
70
71
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
71
72
|
import { buildProvenanceCoverage } from './provenance-coverage.js';
|
|
@@ -143,6 +144,22 @@ function parseCountFlag(value) {
|
|
|
143
144
|
const parsed = parseInt(String(value), 10);
|
|
144
145
|
return Number.isFinite(parsed) && parsed >= 1 ? parsed : 0;
|
|
145
146
|
}
|
|
147
|
+
function parseBudgetFlag(value, fallback) {
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
return fallback;
|
|
150
|
+
// A value-less flag and a junk value are different typos; the --hops guard already splits them.
|
|
151
|
+
if (typeof value !== 'string') {
|
|
152
|
+
console.error('--budget requires an integer value (e.g. --budget 1500).');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
// Number(), like the --hops guard: parseInt('12abc') is 12, silently accepting what this message rejects.
|
|
156
|
+
const parsed = Number(value);
|
|
157
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
158
|
+
console.error(`Invalid --budget: "${value}". Must be a non-negative integer.`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
return parsed;
|
|
162
|
+
}
|
|
146
163
|
/**
|
|
147
164
|
* Emit an audit event against `hippoRoot`'s db. Opens its own short-lived
|
|
148
165
|
* connection so callers don't have to thread a db handle. Swallows all errors
|
|
@@ -233,10 +250,32 @@ async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
233
250
|
throw err;
|
|
234
251
|
}
|
|
235
252
|
}
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
const BOOLEAN_FLAGS = new Set([
|
|
253
|
+
// Every switch the CLI reads. A value on one reads as on under Boolean() (`--fix=false` would fix)
|
|
254
|
+
// and as off under === true (`--pin=true` would not pin), so parseArgs and main() refuse one.
|
|
255
|
+
// tests/cli-parse-flag-equals.test.ts fails when a switch read is missing from this set.
|
|
256
|
+
export const BOOLEAN_FLAGS = new Set([
|
|
257
|
+
'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'continuity',
|
|
258
|
+
'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
|
|
259
|
+
'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
|
|
260
|
+
'help', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
261
|
+
'no-learn', 'no-mmr', 'no-propagate', 'no-schedule', 'no-share', 'no-summarize-older',
|
|
262
|
+
'observed', 'open', 'physics', 'pin', 'pinned-only', 'reject-loser', 'rerank-utility',
|
|
263
|
+
'reset-physics', 'save-baseline', 'show-cases', 'stats', 'stdin',
|
|
264
|
+
'strict', 'suite', 'value-aware', 'verified', 'version', 'why',
|
|
265
|
+
]);
|
|
266
|
+
// Shared by both the separated and glued (`=`) forms so the list can't drift.
|
|
267
|
+
function isRepeatableFlag(key) {
|
|
268
|
+
return key === 'tag' || key === 'artifact' || key === 'link' || key === 'step' || key === 'constraint' || key === 'depends-on';
|
|
269
|
+
}
|
|
270
|
+
function pushRepeatableFlag(flags, key, value) {
|
|
271
|
+
if (Array.isArray(flags[key])) {
|
|
272
|
+
// SAFETY: Array.isArray just confirmed flags[key] is an array; the union has no other array member.
|
|
273
|
+
flags[key].push(value);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
flags[key] = [value];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
240
279
|
export function parseArgs(argv) {
|
|
241
280
|
const [, , command = '', ...rest] = argv;
|
|
242
281
|
const args = [];
|
|
@@ -249,26 +288,43 @@ export function parseArgs(argv) {
|
|
|
249
288
|
break;
|
|
250
289
|
}
|
|
251
290
|
if (part.startsWith('--')) {
|
|
291
|
+
const eqIdx = part.indexOf('=');
|
|
292
|
+
if (eqIdx > 2) {
|
|
293
|
+
// Glued form has no following token to swallow, so BOOLEAN_FLAGS gets its
|
|
294
|
+
// own branch here instead of the swallow-avoidance short-circuit below.
|
|
295
|
+
const key = part.slice(2, eqIdx);
|
|
296
|
+
const value = part.slice(eqIdx + 1);
|
|
297
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
298
|
+
flags[key] = value;
|
|
299
|
+
}
|
|
300
|
+
else if (isRepeatableFlag(key)) {
|
|
301
|
+
if (value !== '')
|
|
302
|
+
pushRepeatableFlag(flags, key, value);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
flags[key] = value === '' ? true : value;
|
|
306
|
+
}
|
|
307
|
+
i++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
252
310
|
const key = part.slice(2);
|
|
253
311
|
const next = rest[i + 1];
|
|
254
|
-
if (
|
|
312
|
+
if (BOOLEAN_FLAGS.has(key) && (next === 'true' || next === 'false')) {
|
|
313
|
+
// Kept as a value so main() rejects it, instead of `--pin true` pinning the text "... true".
|
|
314
|
+
flags[key] = next;
|
|
315
|
+
i += 2;
|
|
316
|
+
}
|
|
317
|
+
else if (!next || next.startsWith('--') || BOOLEAN_FLAGS.has(key)) {
|
|
255
318
|
// Boolean flag
|
|
256
319
|
flags[key] = true;
|
|
257
320
|
i++;
|
|
258
321
|
}
|
|
322
|
+
else if (isRepeatableFlag(key)) {
|
|
323
|
+
pushRepeatableFlag(flags, key, next);
|
|
324
|
+
i += 2;
|
|
325
|
+
}
|
|
259
326
|
else {
|
|
260
|
-
|
|
261
|
-
if (key === 'tag' || key === 'artifact' || key === 'link' || key === 'step' || key === 'constraint' || key === 'depends-on') {
|
|
262
|
-
if (Array.isArray(flags[key])) {
|
|
263
|
-
flags[key].push(next);
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
flags[key] = [next];
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
flags[key] = next;
|
|
271
|
-
}
|
|
327
|
+
flags[key] = next;
|
|
272
328
|
i += 2;
|
|
273
329
|
}
|
|
274
330
|
}
|
|
@@ -768,7 +824,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
768
824
|
}
|
|
769
825
|
async function cmdRecall(hippoRoot, query, flags) {
|
|
770
826
|
requireInit(hippoRoot);
|
|
771
|
-
const budget =
|
|
827
|
+
const budget = parseBudgetFlag(flags['budget'], 4000);
|
|
772
828
|
const limit = parseLimitFlag(flags['limit']);
|
|
773
829
|
const asJson = Boolean(flags['json']);
|
|
774
830
|
const showWhy = Boolean(flags['why']);
|
|
@@ -1895,7 +1951,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1895
1951
|
}
|
|
1896
1952
|
async function cmdExplain(hippoRoot, query, flags) {
|
|
1897
1953
|
requireInit(hippoRoot);
|
|
1898
|
-
const budget =
|
|
1954
|
+
const budget = parseBudgetFlag(flags['budget'], 4000);
|
|
1899
1955
|
const limit = parseLimitFlag(flags['limit']);
|
|
1900
1956
|
const asJson = Boolean(flags['json']);
|
|
1901
1957
|
const forcePhysics = Boolean(flags['physics']);
|
|
@@ -2776,7 +2832,7 @@ function cmdLastSleep(flags) {
|
|
|
2776
2832
|
// X8: session-event content is capped at print time only — the shared
|
|
2777
2833
|
// printSessionEvents stays untouched for every other caller.
|
|
2778
2834
|
const COMPACT_RESUME_EVENT_CONTENT_CAP = 400;
|
|
2779
|
-
function cmdCompactResume(hippoRoot, stdinText) {
|
|
2835
|
+
function cmdCompactResume(hippoRoot, stdinText, stdinTimedOut) {
|
|
2780
2836
|
try {
|
|
2781
2837
|
// X3: gate on the non-exiting isInitialized check before any
|
|
2782
2838
|
// store-opening call (loadActiveTaskSnapshot/listSessionEvents both
|
|
@@ -2791,7 +2847,9 @@ function cmdCompactResume(hippoRoot, stdinText) {
|
|
|
2791
2847
|
// carries a different source (e.g. 'startup') means the matcher-based
|
|
2792
2848
|
// gate failed to apply — stay silent rather than print stale state.
|
|
2793
2849
|
const nonEmptyStdin = !!stdinText && stdinText.trim() !== '';
|
|
2794
|
-
|
|
2850
|
+
// Without a payload session_id the X5 cross-restore guard below can
|
|
2851
|
+
// never fire, so a timed-out empty read must not reach the print path.
|
|
2852
|
+
let suppressOutput = stdinTimedOut && !nonEmptyStdin;
|
|
2795
2853
|
let payloadSessionId = null;
|
|
2796
2854
|
if (nonEmptyStdin) {
|
|
2797
2855
|
let payload = null;
|
|
@@ -2872,30 +2930,23 @@ function cmdCompactResume(hippoRoot, stdinText) {
|
|
|
2872
2930
|
*/
|
|
2873
2931
|
async function cmdSessionEnd(hippoRoot, flags) {
|
|
2874
2932
|
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
2875
|
-
//
|
|
2876
|
-
//
|
|
2877
|
-
// via argv so the detached child doesn't need to inherit stdin.
|
|
2878
|
-
// DF1 T3 (docs/plans/2026-08-23-df1-snapshot-lifecycle.md): `session_id`
|
|
2879
|
-
// is extracted the same way, so the worker can close the ending session's
|
|
2880
|
-
// own active task snapshot after sleep+capture finish.
|
|
2881
|
-
let transcriptPath = null;
|
|
2933
|
+
// Bounded read (DF1 T3, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
2934
|
+
// extracts transcript_path + session_id for the detached worker's argv.
|
|
2882
2935
|
let sessionId = null;
|
|
2936
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
2883
2937
|
try {
|
|
2884
|
-
const stdinText = fs.readFileSync(0, 'utf8');
|
|
2885
2938
|
if (stdinText && stdinText.trim().startsWith('{')) {
|
|
2886
2939
|
const payload = JSON.parse(stdinText);
|
|
2887
|
-
if (typeof payload.transcript_path === 'string') {
|
|
2888
|
-
transcriptPath = payload.transcript_path;
|
|
2889
|
-
}
|
|
2890
2940
|
if (typeof payload.session_id === 'string') {
|
|
2891
2941
|
sessionId = payload.session_id;
|
|
2892
2942
|
}
|
|
2893
2943
|
}
|
|
2894
2944
|
}
|
|
2895
2945
|
catch {
|
|
2896
|
-
// No stdin, not JSON, or read failure
|
|
2897
|
-
// transcript auto-discovery; the snapshot close below will no-op.
|
|
2946
|
+
// No stdin, not JSON, or read failure: the snapshot close below will no-op.
|
|
2898
2947
|
}
|
|
2948
|
+
// Resolved here because only this process saw the payload; the worker captures just the path it is handed.
|
|
2949
|
+
const transcriptPath = resolveLastSessionTranscript(undefined, stdinText, stdinTimedOut);
|
|
2899
2950
|
const workerArgs = [process.argv[1], '__session-end-worker'];
|
|
2900
2951
|
if (logFile)
|
|
2901
2952
|
workerArgs.push('--log-file', logFile);
|
|
@@ -2912,11 +2963,11 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
2912
2963
|
child.unref();
|
|
2913
2964
|
}
|
|
2914
2965
|
catch (err) {
|
|
2915
|
-
// If spawn fails, run inline as a last resort
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2966
|
+
// If spawn fails, run inline as a last resort, handed what the child's argv would have carried.
|
|
2967
|
+
if (transcriptPath)
|
|
2968
|
+
flags['transcript'] = transcriptPath;
|
|
2969
|
+
if (sessionId)
|
|
2970
|
+
flags['session-id'] = sessionId;
|
|
2920
2971
|
await cmdSessionEndWorker(hippoRoot, flags);
|
|
2921
2972
|
return;
|
|
2922
2973
|
}
|
|
@@ -2959,19 +3010,22 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
2959
3010
|
// `[hippo] sleep failed: ...` line. Continue to capture regardless.
|
|
2960
3011
|
}
|
|
2961
3012
|
try {
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3013
|
+
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined;
|
|
3014
|
+
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3015
|
+
// With no stdin of its own, capture would read this as a manual run and scan every project.
|
|
3016
|
+
if (!transcriptPath) {
|
|
3017
|
+
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no transcript for this session');
|
|
3018
|
+
}
|
|
3019
|
+
else {
|
|
3020
|
+
cmdCapture(hippoRoot, {
|
|
3021
|
+
source: 'last-session',
|
|
3022
|
+
transcriptPath,
|
|
3023
|
+
logFile,
|
|
3024
|
+
dryRun: false,
|
|
3025
|
+
global: false,
|
|
3026
|
+
tenantId: resolveTenantId({}),
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
2975
3029
|
}
|
|
2976
3030
|
catch {
|
|
2977
3031
|
// Same treatment — the failure line is already in the log.
|
|
@@ -3154,6 +3208,11 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3154
3208
|
startOffsetBytes,
|
|
3155
3209
|
startedAtMs,
|
|
3156
3210
|
}) ?? undefined;
|
|
3211
|
+
// No Codex transcript must not fall through to the scan of Claude Code projects.
|
|
3212
|
+
if (!transcriptPath) {
|
|
3213
|
+
appendSessionEndCloseLog(logFile ?? null, 'skip capture: no Codex transcript for this session');
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3157
3216
|
const captureOpts = {
|
|
3158
3217
|
source: 'last-session',
|
|
3159
3218
|
transcriptPath,
|
|
@@ -4122,10 +4181,8 @@ function cmdCard(hippoRoot, args, flags) {
|
|
|
4122
4181
|
const allowedFlags = CARD_SUBCOMMAND_FLAGS[subcommand];
|
|
4123
4182
|
for (const key of Object.keys(flags)) {
|
|
4124
4183
|
if (!allowedFlags.includes(key)) {
|
|
4125
|
-
// --flag=value never splits on '=' (see parseArgs), so it lands here as one long key.
|
|
4126
|
-
const hint = key.includes('=') ? ` Use --${key.slice(0, key.indexOf('='))} <value>, not --${key}.` : '';
|
|
4127
4184
|
const valid = allowedFlags.length > 0 ? allowedFlags.map((f) => `--${f}`).join(', ') : '(none)';
|
|
4128
|
-
console.error(`Unknown flag --${key} for hippo card ${subcommand}
|
|
4185
|
+
console.error(`Unknown flag --${key} for hippo card ${subcommand}. Valid flags: ${valid}`);
|
|
4129
4186
|
process.exit(1);
|
|
4130
4187
|
}
|
|
4131
4188
|
}
|
|
@@ -5915,7 +5972,7 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
5915
5972
|
if (!pinnedOnly) {
|
|
5916
5973
|
requireInit(hippoRoot);
|
|
5917
5974
|
}
|
|
5918
|
-
const budget =
|
|
5975
|
+
const budget = parseBudgetFlag(flags['budget'], 1500);
|
|
5919
5976
|
if (budget <= 0)
|
|
5920
5977
|
return;
|
|
5921
5978
|
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
@@ -7242,7 +7299,8 @@ function cmdDag(hippoRoot, flags) {
|
|
|
7242
7299
|
}
|
|
7243
7300
|
function cmdAssemble(hippoRoot, sessionId, flags) {
|
|
7244
7301
|
requireInit(hippoRoot);
|
|
7245
|
-
|
|
7302
|
+
// Absent stays undefined so the api default applies; the 0 fallback is unreachable.
|
|
7303
|
+
const budget = flags['budget'] === undefined ? undefined : parseBudgetFlag(flags['budget'], 0);
|
|
7246
7304
|
const freshTailCount = typeof flags['fresh-tail'] === 'string' ? Number(flags['fresh-tail']) : undefined;
|
|
7247
7305
|
const summarizeOlder = flags['no-summarize-older'] !== true;
|
|
7248
7306
|
const scope = typeof flags['scope'] === 'string' && flags['scope'].length > 0
|
|
@@ -7273,7 +7331,8 @@ function cmdAssemble(hippoRoot, sessionId, flags) {
|
|
|
7273
7331
|
function cmdDrillDown(hippoRoot, summaryId, flags) {
|
|
7274
7332
|
requireInit(hippoRoot);
|
|
7275
7333
|
const limit = typeof flags['limit'] === 'string' ? Number(flags['limit']) : undefined;
|
|
7276
|
-
|
|
7334
|
+
// Absent stays undefined so the api default applies; the 0 fallback is unreachable.
|
|
7335
|
+
const budget = flags['budget'] === undefined ? undefined : parseBudgetFlag(flags['budget'], 0);
|
|
7277
7336
|
// v0.30 / E5: --depth N walks N levels down (default 1, hard cap 10).
|
|
7278
7337
|
// L4 fold: reject out-of-range explicitly (no silent clamp).
|
|
7279
7338
|
const rawDepth = typeof flags['depth'] === 'string' ? Number(flags['depth']) : undefined;
|
|
@@ -8603,6 +8662,29 @@ async function main() {
|
|
|
8603
8662
|
console.error('--scope requires a non-empty value (e.g. --scope slack:private:C1).');
|
|
8604
8663
|
process.exit(1);
|
|
8605
8664
|
}
|
|
8665
|
+
// parseArgs stores a value-less flag as boolean true, and NaN then survives every
|
|
8666
|
+
// downstream guard because each comparison against it is false.
|
|
8667
|
+
const NUMERIC_FLAGS = [
|
|
8668
|
+
'days', 'threshold', 'min-score', 'port', 'limit', 'mmr-lambda', 'local-bump',
|
|
8669
|
+
'min-results', 'reranker-top-k', 'min-mrr', 'embedding-weight', 'max-cases',
|
|
8670
|
+
];
|
|
8671
|
+
for (const key of NUMERIC_FLAGS) {
|
|
8672
|
+
const raw = flags[key];
|
|
8673
|
+
if (raw === undefined)
|
|
8674
|
+
continue;
|
|
8675
|
+
if (typeof raw !== 'string' || !raw.trim() || !Number.isFinite(Number(raw))) {
|
|
8676
|
+
console.error(`--${key} requires a numeric value.`);
|
|
8677
|
+
process.exit(1);
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
// Reject rather than coerce: consumers read --dry-run both as Boolean() and === true,
|
|
8681
|
+
// so no single coercion of an inline value would be correct for every one of them.
|
|
8682
|
+
for (const key of BOOLEAN_FLAGS) {
|
|
8683
|
+
if (Object.hasOwn(flags, key) && typeof flags[key] !== 'boolean') {
|
|
8684
|
+
console.error(`--${key} takes no value`);
|
|
8685
|
+
process.exit(1);
|
|
8686
|
+
}
|
|
8687
|
+
}
|
|
8606
8688
|
switch (command) {
|
|
8607
8689
|
case 'init':
|
|
8608
8690
|
cmdInit(hippoRoot, flags);
|
|
@@ -8747,34 +8829,18 @@ async function main() {
|
|
|
8747
8829
|
await cmdSessionEndWorker(hippoRoot, flags);
|
|
8748
8830
|
break;
|
|
8749
8831
|
case 'pre-compact': {
|
|
8750
|
-
//
|
|
8751
|
-
|
|
8752
|
-
let stdinText;
|
|
8753
|
-
if (!process.stdin.isTTY) {
|
|
8754
|
-
try {
|
|
8755
|
-
stdinText = fs.readFileSync(0, 'utf8');
|
|
8756
|
-
}
|
|
8757
|
-
catch {
|
|
8758
|
-
stdinText = undefined;
|
|
8759
|
-
}
|
|
8760
|
-
}
|
|
8832
|
+
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
8833
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8761
8834
|
await cmdPreCompact(hippoRoot, {
|
|
8762
8835
|
stdinText,
|
|
8836
|
+
stdinTimedOut,
|
|
8763
8837
|
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
8764
8838
|
});
|
|
8765
8839
|
break;
|
|
8766
8840
|
}
|
|
8767
8841
|
case 'compact-resume': {
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
try {
|
|
8771
|
-
stdinText = fs.readFileSync(0, 'utf8');
|
|
8772
|
-
}
|
|
8773
|
-
catch {
|
|
8774
|
-
stdinText = undefined;
|
|
8775
|
-
}
|
|
8776
|
-
}
|
|
8777
|
-
cmdCompactResume(hippoRoot, stdinText);
|
|
8842
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8843
|
+
cmdCompactResume(hippoRoot, stdinText, stdinTimedOut);
|
|
8778
8844
|
break;
|
|
8779
8845
|
}
|
|
8780
8846
|
case 'codex-run':
|
|
@@ -8987,23 +9053,9 @@ async function main() {
|
|
|
8987
9053
|
break;
|
|
8988
9054
|
}
|
|
8989
9055
|
case 'context': {
|
|
8990
|
-
// DF1 T2
|
|
8991
|
-
//
|
|
8992
|
-
|
|
8993
|
-
// hangs. `hippo context` is both the hot UserPromptSubmit path
|
|
8994
|
-
// (non-TTY, stdin carries the hook JSON with `session_id`) and a
|
|
8995
|
-
// manually-invocable command (TTY, no payload) — the guardless read in
|
|
8996
|
-
// cmdSessionEnd is the wrong sibling to copy here; it only runs under
|
|
8997
|
-
// a hook that always supplies stdin.
|
|
8998
|
-
let stdinText;
|
|
8999
|
-
if (!process.stdin.isTTY) {
|
|
9000
|
-
try {
|
|
9001
|
-
stdinText = fs.readFileSync(0, 'utf8');
|
|
9002
|
-
}
|
|
9003
|
-
catch {
|
|
9004
|
-
stdinText = undefined;
|
|
9005
|
-
}
|
|
9006
|
-
}
|
|
9056
|
+
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
9057
|
+
// the hot stdin path and a manual run share this one command.
|
|
9058
|
+
const { text: stdinText } = await readStdinBounded();
|
|
9007
9059
|
await cmdContext(hippoRoot, args, flags, stdinText);
|
|
9008
9060
|
break;
|
|
9009
9061
|
}
|
|
@@ -9180,10 +9232,17 @@ async function main() {
|
|
|
9180
9232
|
console.error('Usage: hippo capture --stdin|--file <path>|--last-session [--transcript <path>] [--log-file <path>] [--dry-run] [--global]');
|
|
9181
9233
|
process.exit(1);
|
|
9182
9234
|
}
|
|
9235
|
+
// Bounded, and only when last-session has no explicit path: the
|
|
9236
|
+
// --stdin source keeps its own blocking read in capture.ts by design.
|
|
9237
|
+
const bounded = captureSource === 'last-session' && !transcriptPath
|
|
9238
|
+
? await readStdinBounded()
|
|
9239
|
+
: { text: undefined, timedOut: false };
|
|
9183
9240
|
cmdCapture(hippoRoot, {
|
|
9184
9241
|
source: captureSource,
|
|
9185
9242
|
filePath: captureFile,
|
|
9186
9243
|
transcriptPath,
|
|
9244
|
+
stdinText: bounded.text,
|
|
9245
|
+
stdinTimedOut: bounded.timedOut,
|
|
9187
9246
|
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
9188
9247
|
dryRun: Boolean(flags['dry-run']),
|
|
9189
9248
|
global: Boolean(flags['global']),
|
|
@@ -9242,8 +9301,8 @@ async function main() {
|
|
|
9242
9301
|
}
|
|
9243
9302
|
const onlyId = typeof flags['id'] === 'string' ? flags['id'] : undefined;
|
|
9244
9303
|
if (typeof flags['dry-run'] === 'string') {
|
|
9245
|
-
//
|
|
9246
|
-
//
|
|
9304
|
+
// Dead: the earlier global BOOLEAN_FLAGS guard now exits first on any --dry-run=<v>.
|
|
9305
|
+
// Kept as defence in depth on a destructive command.
|
|
9247
9306
|
console.error('--dry-run takes no value');
|
|
9248
9307
|
process.exit(1);
|
|
9249
9308
|
}
|