hippo-memory 1.57.0 → 1.59.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 +24 -1
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/agent-memories/legacy.js +4 -1
- package/dist/api-errors.d.ts +27 -0
- package/dist/api-errors.js +37 -0
- package/dist/api.d.ts +5 -5
- package/dist/api.js +40 -47
- package/dist/audit.d.ts +5 -1
- package/dist/audit.js +13 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +7 -5
- package/dist/capture-contract.d.ts +47 -0
- package/dist/capture-contract.js +49 -0
- package/dist/capture-error.js +2 -1
- package/dist/capture.d.ts +0 -13
- package/dist/capture.js +5 -66
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +7 -0
- package/dist/cli/projects.d.ts +4 -0
- package/dist/cli/projects.js +90 -0
- package/dist/cli/shared.js +23 -13
- package/dist/cli/sleep.js +5 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +486 -397
- package/dist/client.js +9 -0
- package/dist/codex-patch.js +1 -1
- package/dist/compaction-record.d.ts +1 -1
- package/dist/compaction-record.js +3 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/github/dlq.js +5 -2
- package/dist/connectors/github/octokit-client.js +4 -2
- package/dist/connectors/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +48 -35
- package/dist/customer-notes.js +14 -13
- package/dist/dag.js +7 -4
- package/dist/dashboard.js +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +9 -8
- package/dist/dedupe.js +1 -1
- package/dist/doctor.js +28 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/extract.js +19 -18
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +8 -0
- package/dist/http-util.js +10 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +11 -10
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +52 -24
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.js +3 -2
- package/dist/overlap-index.d.ts +7 -0
- package/dist/overlap-index.js +38 -0
- package/dist/pilot-arm.d.ts +9 -0
- package/dist/pilot-arm.js +47 -0
- package/dist/policies.js +12 -11
- package/dist/predictions.js +9 -8
- package/dist/processes.js +14 -13
- package/dist/project-briefs.js +16 -15
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/raw-archive.js +7 -6
- package/dist/recall-scope.d.ts +5 -4
- package/dist/recall-scope.js +7 -5
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.js +6 -9
- package/dist/rejection.d.ts +2 -1
- package/dist/rejection.js +2 -1
- package/dist/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +182 -0
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +11 -0
- package/dist/rerankers/jev.js +10 -5
- package/dist/rerankers/types.d.ts +16 -0
- package/dist/search.js +14 -2
- package/dist/secret-detect.d.ts +13 -1
- package/dist/secret-detect.js +33 -1
- package/dist/server.d.ts +9 -2
- package/dist/server.js +188 -411
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +15 -14
- package/dist/store.js +10 -10
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- 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 +5 -2
- package/dist/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The shared shape every agent adapter turns a host payload into, so capture code is written once for all runtimes.
|
|
2
|
+
// Field meanings, statuses and how to add a runtime: docs/integrations/agent-inventory.md.
|
|
3
|
+
/** A string guard that avoids `typeof`; it matches it for every value `JSON.parse` can produce. */
|
|
4
|
+
export function isStringValue(value) {
|
|
5
|
+
return String(value) === value;
|
|
6
|
+
}
|
|
7
|
+
/** An object guard that avoids `typeof`; arrays count as objects, as they do for `typeof`. */
|
|
8
|
+
export function isObjectLike(value) {
|
|
9
|
+
return value !== null && value instanceof Object;
|
|
10
|
+
}
|
|
11
|
+
/** Reads a Claude Code PreCompact payload; only an empty stdin counts as a manual run. */
|
|
12
|
+
export function readClaudeCodePreCompact(stdinText, timedOut) {
|
|
13
|
+
const empty = !stdinText || stdinText.trim() === '';
|
|
14
|
+
if (timedOut && empty) {
|
|
15
|
+
return { status: 'unavailable', reason: 'no PreCompact payload arrived before the stdin wait window closed' };
|
|
16
|
+
}
|
|
17
|
+
const base = { runtime: 'claude-code', event: 'pre-compact' };
|
|
18
|
+
if (empty) {
|
|
19
|
+
return { status: 'received', input: { ...base, manual: true, sessionId: null, cwd: null, transcriptPath: null, trigger: null } };
|
|
20
|
+
}
|
|
21
|
+
let payload;
|
|
22
|
+
try {
|
|
23
|
+
payload = JSON.parse(stdinText.trim());
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Non-JSON stdin leaves payload undefined, which the shape check rejects like an explicit null.
|
|
27
|
+
}
|
|
28
|
+
// A bad payload is skipped, never treated as manual: discovery could pick up another session's transcript.
|
|
29
|
+
if (!isObjectLike(payload) || !('transcript_path' in payload) || !isStringValue(payload.transcript_path)) {
|
|
30
|
+
return { status: 'skipped', reason: 'malformed or incomplete PreCompact payload (missing string transcript_path)' };
|
|
31
|
+
}
|
|
32
|
+
const transcriptPath = payload.transcript_path;
|
|
33
|
+
// Path shape only: CLAUDE_CONFIG_DIR can move the transcript root, and a same-user process can already read every transcript.
|
|
34
|
+
if (!/\.jsonl$/i.test(transcriptPath)) {
|
|
35
|
+
return { status: 'skipped', reason: `payload transcript_path is not a .jsonl file: ${transcriptPath}` };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
status: 'received',
|
|
39
|
+
input: {
|
|
40
|
+
...base,
|
|
41
|
+
manual: false,
|
|
42
|
+
sessionId: 'session_id' in payload && isStringValue(payload.session_id) ? payload.session_id : null,
|
|
43
|
+
cwd: 'cwd' in payload && isStringValue(payload.cwd) ? payload.cwd : null,
|
|
44
|
+
transcriptPath,
|
|
45
|
+
trigger: 'trigger' in payload && isStringValue(payload.trigger) ? payload.trigger : null,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=capture-contract.js.map
|
package/dist/capture-error.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadConfig } from './config.js';
|
|
|
7
7
|
import { closeHippoDb, openHippoDb } from './db.js';
|
|
8
8
|
import { recordFailure } from './failure-log.js';
|
|
9
9
|
import { blockHash } from './token-ledger.js';
|
|
10
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
10
11
|
const MAX_LEN = 200;
|
|
11
12
|
/** The user, a permission prompt or a hook said no: routine, not a lesson. */
|
|
12
13
|
const DECLINED = /user (?:doesn't|does not) want|denied by (?:the )?user|user (?:rejected|declined|denied)|permission to use|was blocked by (?:a )?hook/i;
|
|
@@ -44,7 +45,7 @@ export function lessonFromFailure(payload) {
|
|
|
44
45
|
if (!isString(p.error) || p.error.trim().length < 12)
|
|
45
46
|
return { skip: 'skipped-invalid', text: null, detail: null };
|
|
46
47
|
const tool = isString(p.tool_name) ? p.tool_name : 'tool';
|
|
47
|
-
const error = p.error.replace(/\s+/g, ' ').trim();
|
|
48
|
+
const error = redactSecretsStrict(p.error.replace(/\s+/g, ' ').trim());
|
|
48
49
|
const text = `${tool}: ${error}`.slice(0, MAX_LEN);
|
|
49
50
|
const command = isObject(p.tool_input) && isString(p.tool_input['command']) ? p.tool_input['command'].replace(LEADING_CD, '') : '';
|
|
50
51
|
const head = command.trim().split(/\s+/).slice(0, 2).join(' ');
|
package/dist/capture.d.ts
CHANGED
|
@@ -49,19 +49,6 @@ export interface CaptureOptions {
|
|
|
49
49
|
tenantId?: string;
|
|
50
50
|
originProject?: string;
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Runtime shape guards used throughout this file wherever a value arrives
|
|
54
|
-
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
55
|
-
* over the input so the parameter is never annotated `unknown` directly — TS
|
|
56
|
-
* infers it from the call site — while the check itself avoids `typeof` by
|
|
57
|
-
* testing identity against the coercion (`String(x) === x`) /
|
|
58
|
-
* prototype-chain (`instanceof Object`) instead. Behaviourally equivalent to
|
|
59
|
-
* `typeof x === 'string'` / a truthy `typeof x === 'object'` check for
|
|
60
|
-
* anything `JSON.parse` can produce (the only divergence is boxed
|
|
61
|
-
* primitives, which JSON.parse never yields).
|
|
62
|
-
*/
|
|
63
|
-
export declare function isStringValue<T>(value: T): value is T & string;
|
|
64
|
-
export declare function isObjectLike<T>(value: T): value is T & object;
|
|
65
52
|
/** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
|
|
66
53
|
export declare function errorMessage(cause: unknown): string;
|
|
67
54
|
/** Transcript-line flags isNonHumanUserLine reads (any may be absent); `promptSource: 'system'` marks Claude Code's own notices. */
|
package/dist/capture.js
CHANGED
|
@@ -25,6 +25,7 @@ import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
|
25
25
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
26
26
|
import { loadConfig } from './config.js';
|
|
27
27
|
import { classifyOriginProject } from './project-identity.js';
|
|
28
|
+
import { isObjectLike, isStringValue, readClaudeCodePreCompact } from './capture-contract.js';
|
|
28
29
|
// Sentence-level patterns
|
|
29
30
|
//
|
|
30
31
|
// T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
|
|
@@ -471,23 +472,6 @@ export function extractFromText(text) {
|
|
|
471
472
|
}
|
|
472
473
|
return items;
|
|
473
474
|
}
|
|
474
|
-
/**
|
|
475
|
-
* Runtime shape guards used throughout this file wherever a value arrives
|
|
476
|
-
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
477
|
-
* over the input so the parameter is never annotated `unknown` directly — TS
|
|
478
|
-
* infers it from the call site — while the check itself avoids `typeof` by
|
|
479
|
-
* testing identity against the coercion (`String(x) === x`) /
|
|
480
|
-
* prototype-chain (`instanceof Object`) instead. Behaviourally equivalent to
|
|
481
|
-
* `typeof x === 'string'` / a truthy `typeof x === 'object'` check for
|
|
482
|
-
* anything `JSON.parse` can produce (the only divergence is boxed
|
|
483
|
-
* primitives, which JSON.parse never yields).
|
|
484
|
-
*/
|
|
485
|
-
export function isStringValue(value) {
|
|
486
|
-
return String(value) === value;
|
|
487
|
-
}
|
|
488
|
-
export function isObjectLike(value) {
|
|
489
|
-
return value !== null && value instanceof Object;
|
|
490
|
-
}
|
|
491
475
|
/** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
|
|
492
476
|
export function errorMessage(cause) {
|
|
493
477
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -1161,57 +1145,12 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
1161
1145
|
appendPreCompactLog(logFile, 'skip: store not initialized');
|
|
1162
1146
|
return;
|
|
1163
1147
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
appendPreCompactLog(logFile, 'skip: no PreCompact payload arrived before the stdin wait window closed');
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
// A true manual invocation has no stdin at all (TTY, or a non-TTY pipe
|
|
1171
|
-
// that yielded an empty read) — that's the ONLY case newest-transcript
|
|
1172
|
-
// auto-discovery is allowed to run. Any other non-empty stdin must
|
|
1173
|
-
// JSON-parse to an object carrying a string transcript_path, or it is
|
|
1174
|
-
// treated as malformed input and skipped (X4) rather than silently
|
|
1175
|
-
// falling back to discovery, which could snapshot an unrelated session's
|
|
1176
|
-
// transcript under this payload's session_id.
|
|
1177
|
-
const manualInvocation = !stdinText || stdinText.trim() === '';
|
|
1178
|
-
let sessionId = null;
|
|
1179
|
-
let payloadTranscriptPath = null;
|
|
1180
|
-
let payloadCwd = null;
|
|
1181
|
-
let payloadTrigger = null;
|
|
1182
|
-
if (!manualInvocation) {
|
|
1183
|
-
let payload;
|
|
1184
|
-
try {
|
|
1185
|
-
payload = JSON.parse(stdinText.trim());
|
|
1186
|
-
}
|
|
1187
|
-
catch {
|
|
1188
|
-
// payload stays undefined; the isObjectLike check below rejects it
|
|
1189
|
-
// the same way it would reject an explicit null.
|
|
1190
|
-
}
|
|
1191
|
-
if (!isObjectLike(payload) || !('transcript_path' in payload) || !isStringValue(payload.transcript_path)) {
|
|
1192
|
-
// Covers non-JSON stdin, a JSON value that isn't an object, and
|
|
1193
|
-
// `"transcript_path": null` (or the key missing entirely) — all fail
|
|
1194
|
-
// the string check. Log and skip; never fall through to auto-discovery.
|
|
1195
|
-
appendPreCompactLog(logFile, 'skip: malformed or incomplete PreCompact payload (missing string transcript_path)');
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
if ('session_id' in payload && isStringValue(payload.session_id))
|
|
1199
|
-
sessionId = payload.session_id;
|
|
1200
|
-
payloadTranscriptPath = payload.transcript_path;
|
|
1201
|
-
payloadCwd = 'cwd' in payload && isStringValue(payload.cwd) ? payload.cwd : null;
|
|
1202
|
-
payloadTrigger = 'trigger' in payload && isStringValue(payload.trigger) ? payload.trigger : null;
|
|
1203
|
-
}
|
|
1204
|
-
// X11: payload transcript_path must end .jsonl. No directory-containment
|
|
1205
|
-
// check is applied on top of this — CLAUDE_CONFIG_DIR can relocate the
|
|
1206
|
-
// transcript root entirely, so a path-prefix allowlist would just reject
|
|
1207
|
-
// legitimate relocated installs. The trust boundary here is process
|
|
1208
|
-
// identity, not path shape: a local process able to feed this hook
|
|
1209
|
-
// arbitrary stdin already runs as the same user who owns every transcript
|
|
1210
|
-
// this check could gate on, so containment buys no real isolation.
|
|
1211
|
-
if (payloadTranscriptPath !== null && !/\.jsonl$/i.test(payloadTranscriptPath)) {
|
|
1212
|
-
appendPreCompactLog(logFile, `skip: payload transcript_path is not a .jsonl file: ${payloadTranscriptPath}`);
|
|
1148
|
+
const receipt = readClaudeCodePreCompact(stdinText, stdinTimedOut);
|
|
1149
|
+
if (receipt.status !== 'received') {
|
|
1150
|
+
appendPreCompactLog(logFile, `skip: ${receipt.reason}`);
|
|
1213
1151
|
return;
|
|
1214
1152
|
}
|
|
1153
|
+
const { sessionId, transcriptPath: payloadTranscriptPath, cwd: payloadCwd, trigger: payloadTrigger } = receipt.input;
|
|
1215
1154
|
// The record is the "something saved before every compaction", so it lands even when no snapshot is derivable below.
|
|
1216
1155
|
let recordId = null;
|
|
1217
1156
|
if (sessionId !== null && sessionId !== '') {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// User-facing CLI messages (usage, not-found, refusals) print at every HIPPO_LOG level; diagnostics go to `log`.
|
|
2
|
+
// Both forward to console.error so the bytes match the old calls and console spies in tests still see them.
|
|
3
|
+
/** Writes a message the user asked for, or must act on, to stderr. */
|
|
4
|
+
export function printError(...args) {
|
|
5
|
+
console.error(...args);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The `hippo projects` verb: list a store's project names, fold an old worktree name into its repo, repair sleep's user-global merges.
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { closeHippoDb, openHippoDb } from '../db.js';
|
|
5
|
+
import { listProjects, mergeProjects, repairUserGlobalMerges } from '../project-merge.js';
|
|
6
|
+
import { resolveTenantId } from '../tenant.js';
|
|
7
|
+
import { resolveAuthRoot } from './shared.js';
|
|
8
|
+
import { printError } from './output.js';
|
|
9
|
+
/** Old per-worktree project names of the repo at cwd, mapped to the repo's main checkout name; empty outside git. */
|
|
10
|
+
function worktreeNames() {
|
|
11
|
+
try {
|
|
12
|
+
const out = execFileSync('git', ['worktree', 'list', '--porcelain'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
13
|
+
const paths = out.split(/\r?\n/).filter((l) => l.startsWith('worktree ')).map((l) => path.basename(l.slice('worktree '.length)));
|
|
14
|
+
return new Map(paths.slice(1).map((name) => [name, paths[0]]));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Outside a git checkout, or no git on PATH: the list just has no worktree hints.
|
|
18
|
+
return new Map();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function label(origin) {
|
|
22
|
+
return origin === null ? '(unknown)' : origin === '' ? '(user-global)' : origin;
|
|
23
|
+
}
|
|
24
|
+
function hint(p, worktrees) {
|
|
25
|
+
const main = p.origin ? worktrees.get(p.origin) : undefined;
|
|
26
|
+
if (main)
|
|
27
|
+
return `\n a worktree of ${main}: hippo projects merge ${p.origin} ${main}`;
|
|
28
|
+
return p.copiesElsewhere > 0 ? `\n ${p.copiesElsewhere} of its imported notes are copies also held under another name` : '';
|
|
29
|
+
}
|
|
30
|
+
function count(n, one, many = `${one}s`) {
|
|
31
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
32
|
+
}
|
|
33
|
+
export function cmdProjects(hippoRoot, args, flags) {
|
|
34
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
35
|
+
const tenantId = resolveTenantId({});
|
|
36
|
+
const apply = flags['apply'] === true;
|
|
37
|
+
const sub = args[0] ?? 'list';
|
|
38
|
+
const db = openHippoDb(root);
|
|
39
|
+
try {
|
|
40
|
+
if (sub === 'list') {
|
|
41
|
+
const projects = listProjects(db, tenantId);
|
|
42
|
+
if (flags['json']) {
|
|
43
|
+
console.log(JSON.stringify({ store: root, projects }, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const worktrees = worktreeNames();
|
|
47
|
+
console.log(`${count(projects.length, 'project name')} in ${root} (newest write first):\n`);
|
|
48
|
+
for (const p of projects) {
|
|
49
|
+
console.log(`${label(p.origin)} ${count(p.live, 'memory', 'memories')}, ${p.imported} imported from agent notes, newest ${p.newest.slice(0, 10)}${hint(p, worktrees)}`);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (sub === 'merge') {
|
|
54
|
+
const [from, into] = [args[1] ?? '', args[2] ?? ''];
|
|
55
|
+
const r = mergeProjects(db, root, { tenantId, from, into, dryRun: !apply });
|
|
56
|
+
if (flags['json']) {
|
|
57
|
+
console.log(JSON.stringify(r, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`${apply ? 'Merged' : 'Dry run: would merge'} ${from} into ${into}:`);
|
|
61
|
+
console.log(` ${count(r.setAside.length, 'imported note copy', 'imported note copies')} set aside (dormant; the next sync under ${into} imports the notes still on disk)`);
|
|
62
|
+
console.log(` ${count(r.restamped.length, 'memory', 'memories')} re-tagged, plus ${r.dormantRestamped.length} dormant and ${count(r.compactions, 'compaction record')}`);
|
|
63
|
+
console.log(apply ? `Backup: ${r.backup}\nEvery id is in the audit log: hippo audit list --op project_merge` : 'Nothing written. Add --apply to run it.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (sub === 'repair') {
|
|
67
|
+
const r = repairUserGlobalMerges(db, root, { tenantId, dryRun: !apply });
|
|
68
|
+
if (flags['json']) {
|
|
69
|
+
console.log(JSON.stringify(r, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log(`${apply ? 'Repaired' : 'Dry run: would repair'} sleep's user-global merged rows:`);
|
|
73
|
+
console.log(` ${r.toProject.length} re-tagged to their parents' project`);
|
|
74
|
+
console.log(` ${r.setAside.length} set aside (parents in two projects; sleep re-merges them per project)`);
|
|
75
|
+
console.log(` ${r.untraced.length} left as they are (no parent left to show which project; check them with hippo inspect <id>)`);
|
|
76
|
+
console.log(apply ? `Backup: ${r.backup}\nEvery id is in the audit log: hippo audit list --op project_repair` : 'Nothing written. Add --apply to run it.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
printError('Usage: hippo projects [list] [--json] | merge <from> <into> [--apply] | repair [--apply] [--global]');
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
closeHippoDb(db);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=projects.js.map
|
package/dist/cli/shared.js
CHANGED
|
@@ -10,7 +10,7 @@ import { RejectedValueError } from '../rejection.js';
|
|
|
10
10
|
import { explainMatch } from '../search.js';
|
|
11
11
|
import { embedMemory } from '../embeddings.js';
|
|
12
12
|
import { loadConfig } from '../config.js';
|
|
13
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
13
|
+
import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy } from '../db.js';
|
|
14
14
|
import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
|
|
15
15
|
import { isGitRepo, fetchGitLog, extractLessons, partitionLessons } from '../autolearn.js';
|
|
16
16
|
import { storedTextKeys, duplicateKey } from '../same-text.js';
|
|
@@ -22,12 +22,14 @@ import { extractPathTags } from '../path-context.js';
|
|
|
22
22
|
import { getGlobalRoot, initGlobal } from '../shared.js';
|
|
23
23
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun } from '../scheduler.js';
|
|
24
24
|
import { sanitizeLogMessage } from '../capture.js';
|
|
25
|
-
import { appendAuditEvent } from '../audit.js';
|
|
25
|
+
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
26
26
|
import { createHash } from 'node:crypto';
|
|
27
27
|
import * as client from '../client.js';
|
|
28
28
|
import { detectServer, removePidfileIfOwned } from '../server-detect.js';
|
|
29
29
|
import { resolveTenantId } from '../tenant.js';
|
|
30
30
|
import { snapshotText, sessionTrailText, handoffText } from '../context-render.js';
|
|
31
|
+
import { log } from '../log.js';
|
|
32
|
+
import { printError } from './output.js';
|
|
31
33
|
export function parseLimitFlag(value) {
|
|
32
34
|
if (!value)
|
|
33
35
|
return Infinity;
|
|
@@ -45,13 +47,13 @@ export function parseBudgetFlag(value, fallback) {
|
|
|
45
47
|
return fallback;
|
|
46
48
|
// A value-less flag and a junk value are different typos; the --hops guard already splits them.
|
|
47
49
|
if (typeof value !== 'string') {
|
|
48
|
-
|
|
50
|
+
printError('--budget requires an integer value (e.g. --budget 1500).');
|
|
49
51
|
process.exit(1);
|
|
50
52
|
}
|
|
51
53
|
// Number(), like the --hops guard: parseInt('12abc') is 12, silently accepting what this message rejects.
|
|
52
54
|
const parsed = Number(value);
|
|
53
55
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
54
|
-
|
|
56
|
+
printError(`Invalid --budget: "${value}". Must be a non-negative integer.`);
|
|
55
57
|
process.exit(1);
|
|
56
58
|
}
|
|
57
59
|
return parsed;
|
|
@@ -77,13 +79,14 @@ export function emitCliAudit(hippoRoot, op, targetId, metadata) {
|
|
|
77
79
|
closeHippoDb(db);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
catch {
|
|
81
|
-
//
|
|
82
|
+
catch (error) {
|
|
83
|
+
// Best effort: the command already did its work.
|
|
84
|
+
reportAuditWriteFailure(op, String(error), targetId);
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
export function requireInit(hippoRoot) {
|
|
85
88
|
if (!isInitialized(hippoRoot)) {
|
|
86
|
-
|
|
89
|
+
printError(`No hippo store at ${hippoRoot} (searched ${process.cwd()} and its parents up to your home directory). Run \`hippo init\` first.`);
|
|
87
90
|
process.exit(1);
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -148,7 +151,7 @@ export async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
148
151
|
const failure = client.classifyTransportFailure(err);
|
|
149
152
|
if (failure === 'never-sent') {
|
|
150
153
|
failIfServerRequired('the server pidfile was stale (connection refused)');
|
|
151
|
-
|
|
154
|
+
log.warn('stale server pidfile detected, falling back to direct mode');
|
|
152
155
|
// Clear the pidfile only if it still names the dead server we just
|
|
153
156
|
// probed — a newer server may have rewritten it (removePidfileIfOwned).
|
|
154
157
|
removePidfileIfOwned(hippoRoot, { pid: info.pid, startedAt: info.started_at });
|
|
@@ -158,7 +161,7 @@ export async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
158
161
|
// Every caller of this helper is a non-idempotent write, so replaying on
|
|
159
162
|
// the direct path would store a row the server may already have committed.
|
|
160
163
|
// Leave the pidfile alone: the next command's connect-phase failure heals it.
|
|
161
|
-
|
|
164
|
+
printError(`hippo: the connection to ${info.url} dropped or timed out mid-request, so the write may already have been applied. Not retrying locally. Check with \`hippo recall\` before running this again.`);
|
|
162
165
|
process.exit(1);
|
|
163
166
|
}
|
|
164
167
|
throw err;
|
|
@@ -215,7 +218,7 @@ export function printAgentImport(report, indent = ' ') {
|
|
|
215
218
|
if (line !== null)
|
|
216
219
|
console.log(`${indent}${line}`);
|
|
217
220
|
for (const warning of report.warnings)
|
|
218
|
-
|
|
221
|
+
printError(`hippo: agent memories: ${warning}`);
|
|
219
222
|
}
|
|
220
223
|
/** The first hippo block in `text` and the agent whose current or shipped text it is; `owner` is undefined for an edited block. */
|
|
221
224
|
export function hippoBlock(text) {
|
|
@@ -300,6 +303,7 @@ export function setupDailySchedule(globalRoot) {
|
|
|
300
303
|
console.log(` Scheduled machine-level daily runner (6:15am) via crontab`);
|
|
301
304
|
}
|
|
302
305
|
catch {
|
|
306
|
+
// No crontab or no permission: print the line for the user to add by hand.
|
|
303
307
|
const cronLine = `15 6 * * * ${cmd}`;
|
|
304
308
|
console.log(` To schedule the machine-level daily runner, add to crontab (crontab -e):`);
|
|
305
309
|
console.log(` ${cronLine}`);
|
|
@@ -309,7 +313,7 @@ export function setupDailySchedule(globalRoot) {
|
|
|
309
313
|
export function parseAsOfFlag(flags) {
|
|
310
314
|
const asOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
311
315
|
if (asOf !== undefined && Number.isNaN(new Date(asOf).getTime())) {
|
|
312
|
-
|
|
316
|
+
printError(`Error: --as-of value "${asOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
313
317
|
process.exit(1);
|
|
314
318
|
}
|
|
315
319
|
return asOf;
|
|
@@ -341,6 +345,7 @@ export function collectHandoffEvidence(cwd, testStatus) {
|
|
|
341
345
|
}).trim() || null;
|
|
342
346
|
}
|
|
343
347
|
catch {
|
|
348
|
+
// No git, not a repo, or timed out: evidence is optional, so the field stays null.
|
|
344
349
|
gitRef = null;
|
|
345
350
|
}
|
|
346
351
|
let dirtyTree = null;
|
|
@@ -351,6 +356,7 @@ export function collectHandoffEvidence(cwd, testStatus) {
|
|
|
351
356
|
dirtyTree = status.trim().length > 0;
|
|
352
357
|
}
|
|
353
358
|
catch {
|
|
359
|
+
// Same as gitRef: unknown tree state is reported as null, never as an error.
|
|
354
360
|
dirtyTree = null;
|
|
355
361
|
}
|
|
356
362
|
return { gitRef, dirtyTree, testStatus };
|
|
@@ -407,7 +413,7 @@ export function cardStringFlag(flags, key) {
|
|
|
407
413
|
if (v === undefined)
|
|
408
414
|
return undefined;
|
|
409
415
|
if (v === true || v === false || Array.isArray(v)) {
|
|
410
|
-
|
|
416
|
+
printError(`--${key} requires a value`);
|
|
411
417
|
process.exit(1);
|
|
412
418
|
}
|
|
413
419
|
return v.trim();
|
|
@@ -476,6 +482,7 @@ export function withLedgerDb(hippoRoot, fn) {
|
|
|
476
482
|
root = getGlobalRoot();
|
|
477
483
|
}
|
|
478
484
|
catch {
|
|
485
|
+
// An unreadable store root means no ledger write; the ledger must never break context or recall.
|
|
479
486
|
return undefined;
|
|
480
487
|
}
|
|
481
488
|
if (root === null)
|
|
@@ -485,7 +492,10 @@ export function withLedgerDb(hippoRoot, fn) {
|
|
|
485
492
|
db = openHippoDb(root);
|
|
486
493
|
return fn(db);
|
|
487
494
|
}
|
|
488
|
-
catch {
|
|
495
|
+
catch (error) {
|
|
496
|
+
// Best effort, but a busy store is the one failure an operator can act on, so it warns once.
|
|
497
|
+
if (isSqliteBusy(error))
|
|
498
|
+
noteStoreBusy('token ledger row skipped');
|
|
489
499
|
return undefined;
|
|
490
500
|
}
|
|
491
501
|
finally {
|
package/dist/cli/sleep.js
CHANGED
|
@@ -8,7 +8,9 @@ import { replayCompactionsAt } from '../compaction-record.js';
|
|
|
8
8
|
import * as api from '../api.js';
|
|
9
9
|
import { resolveTenantId } from '../tenant.js';
|
|
10
10
|
import { renderAmbientSummary } from '../ambient.js';
|
|
11
|
+
import { log } from '../log.js';
|
|
11
12
|
import { requireInit, learnFromRepo, runChurnStaleForRepo, printAgentImport } from './shared.js';
|
|
13
|
+
import { printError } from './output.js';
|
|
12
14
|
/** Runs `hippo sleep`; with `--log-file` it also tees its output to that file. */
|
|
13
15
|
export async function cmdSleep(hippoRoot, flags) {
|
|
14
16
|
// Tee stdout/stderr to a log file when --log-file is set. The SessionEnd
|
|
@@ -45,7 +47,7 @@ export async function cmdSleep(hippoRoot, flags) {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
catch (err) {
|
|
48
|
-
|
|
50
|
+
log.warn(`could not open log file ${logFile}: ${err.message}`);
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
try {
|
|
@@ -145,14 +147,14 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
145
147
|
if (result.marked > 0)
|
|
146
148
|
console.log(`Tagged ${result.marked} memories churn-stale in ${root}.`);
|
|
147
149
|
if (result.error)
|
|
148
|
-
|
|
150
|
+
printError(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }), '');
|
|
152
154
|
}
|
|
153
155
|
// Finishes compactions a killed or busy post-compact hook left; never throws, and a dry run writes nothing.
|
|
154
156
|
if (!flags['dry-run']) {
|
|
155
|
-
const finished = replayCompactionsAt(hippoRoot, (message) =>
|
|
157
|
+
const finished = replayCompactionsAt(hippoRoot, (message) => log.warn(`compaction replay: ${message}`));
|
|
156
158
|
if (finished > 0)
|
|
157
159
|
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over from earlier sessions.`);
|
|
158
160
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* hippo rejections
|
|
21
21
|
* hippo unreject <digest-prefix>
|
|
22
22
|
* hippo dormant [<query>] [--limit <n>] [--json] | restore <id> | forget <id>
|
|
23
|
+
* hippo projects [--json] | merge <from> <into> [--apply] | repair [--apply]
|
|
23
24
|
* hippo tokens [--days <n>] [--json] [--global]
|
|
24
25
|
* hippo doctor [--json]
|
|
25
26
|
* hippo inspect <id>
|