hippo-memory 1.63.2 → 1.64.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/README.md +11 -5
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/maintenance.d.ts +2 -1
- package/dist/cli/maintenance.js +17 -11
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/embedding-provider.js +4 -3
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/local-embedding.d.ts +2 -0
- package/dist/local-embedding.js +20 -4
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- 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 +8 -4
package/dist/hooks/json-hooks.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* sequence, writing both outputs to the log file. The parent returns in
|
|
11
11
|
* <100ms so the TUI teardown can't kill the child before it finishes.
|
|
12
12
|
* - SessionStart: `hippo last-sleep --path <path>` - prints the log
|
|
13
|
-
* written by the previous session's detached worker
|
|
14
|
-
*
|
|
13
|
+
* written by the previous session's detached worker on stderr and
|
|
14
|
+
* clears it. Stdout carries only a `systemMessage` problems line,
|
|
15
|
+
* because Claude Code shows that to the user, not the model.
|
|
15
16
|
* Earlier Claude Code forms are detected and migrated automatically:
|
|
16
17
|
* - < 0.20.2: `Stop` hook firing `hippo sleep` on every assistant turn.
|
|
17
18
|
* - < 0.21.0: bare `hippo sleep` in SessionEnd, no `--log-file`.
|
|
@@ -34,15 +35,18 @@
|
|
|
34
35
|
*/
|
|
35
36
|
import * as fs from 'fs';
|
|
36
37
|
import * as path from 'path';
|
|
37
|
-
import { isJsonObject, HIPPO_SLEEP_MARKER, HIPPO_LAST_SLEEP_MARKER, HIPPO_CAPTURE_MARKER, HIPPO_SESSION_END_MARKER, HIPPO_PINNED_INJECT_MARKER, HIPPO_PINNED_INJECT_COMMAND, HIPPO_PRE_COMPACT_MARKER, HIPPO_COMPACT_RESUME_MARKER, HIPPO_CAPTURE_ERROR_MARKER, HIPPO_POST_COMPACT_MARKER, homeDir, codexHomeDir, defaultPreCompactLogPath } from './shared.js';
|
|
38
|
-
import { isJsonString } from '../json.js';
|
|
38
|
+
import { isJsonObject, HIPPO_SLEEP_MARKER, HIPPO_LAST_SLEEP_MARKER, HIPPO_CAPTURE_MARKER, HIPPO_SESSION_END_MARKER, HIPPO_PINNED_INJECT_MARKER, HIPPO_PINNED_INJECT_COMMAND, HIPPO_PRE_COMPACT_MARKER, HIPPO_COMPACT_RESUME_MARKER, HIPPO_CAPTURE_ERROR_MARKER, HIPPO_POST_COMPACT_MARKER, homeDir, claudeConfigDir, codexHomeDir, defaultPreCompactLogPath } from './shared.js';
|
|
39
|
+
import { isJsonString, readJsonFile } from '../json.js';
|
|
40
|
+
import { escapeRegex } from '../escape.js';
|
|
41
|
+
import { errorMessage, log } from '../log.js';
|
|
42
|
+
/** The paths for `target` under the current home directory; reads and writes nothing. */
|
|
39
43
|
export function resolveJsonHookPaths(target) {
|
|
40
44
|
const home = homeDir();
|
|
41
45
|
const logsDir = path.join(home, '.hippo', 'logs');
|
|
42
46
|
switch (target) {
|
|
43
47
|
case 'claude-code':
|
|
44
48
|
return {
|
|
45
|
-
settings: path.join(
|
|
49
|
+
settings: path.join(claudeConfigDir(), 'settings.json'),
|
|
46
50
|
logFile: path.join(logsDir, 'claude-code-sleep.log'),
|
|
47
51
|
display: 'Claude Code',
|
|
48
52
|
};
|
|
@@ -54,13 +58,58 @@ export function resolveJsonHookPaths(target) {
|
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// An unterminated quote runs to the end, as in a shell, which also keeps the scan linear.
|
|
62
|
+
const QUOTED_SPAN = /'[^']*(?:'|$)|"(?:[^"\\]|\\[\s\S]?)*(?:"|$)/g;
|
|
63
|
+
/** Hippo's command only at string start or after `(`, past env assignments and an install path, with quoted text ignored: `say hippo sleep`, `echo "a; hippo sleep"` and `./backup.sh && hippo sleep` stay a user's. */
|
|
64
|
+
function isHippoCommand(command, marker) {
|
|
65
|
+
const unquoted = command.replace(QUOTED_SPAN, '');
|
|
66
|
+
return new RegExp(`(?:^|\\()\\s*(?:[A-Za-z_]\\w*=\\S*\\s+)*(?:[^\\s(]*[/\\\\])?${escapeRegex(marker)}(?![\\w.-])`).test(unquoted);
|
|
67
|
+
}
|
|
68
|
+
const runsHippo = (...markers) => (command) => markers.some((m) => isHippoCommand(command, m));
|
|
69
|
+
function handlerCommand(handler) {
|
|
70
|
+
return isJsonObject(handler) && isJsonString(handler.command) ? handler.command : null;
|
|
71
|
+
}
|
|
72
|
+
/** `groups` minus the handlers `isOurs` picks; a group goes only once it has no handler left. Null when no handler matched. */
|
|
73
|
+
function withoutHandlers(groups, isOurs) {
|
|
74
|
+
let removed = false;
|
|
75
|
+
const kept = groups.flatMap((group) => {
|
|
76
|
+
if (!isJsonObject(group) || !Array.isArray(group.hooks))
|
|
77
|
+
return [group];
|
|
78
|
+
const handlers = group.hooks.filter((h) => {
|
|
79
|
+
const command = handlerCommand(h);
|
|
80
|
+
return command === null || !isOurs(command);
|
|
81
|
+
});
|
|
82
|
+
if (handlers.length === group.hooks.length)
|
|
83
|
+
return [group];
|
|
84
|
+
removed = true;
|
|
85
|
+
return handlers.length > 0 ? [{ ...group, hooks: handlers }] : [];
|
|
86
|
+
});
|
|
87
|
+
return removed ? kept : null;
|
|
88
|
+
}
|
|
89
|
+
/** Removes the matching handlers from one event's groups; the event key goes once no group is left. */
|
|
90
|
+
function stripHandlers(hooks, event, isOurs) {
|
|
91
|
+
const groups = hooks[event];
|
|
92
|
+
if (!Array.isArray(groups))
|
|
93
|
+
return false;
|
|
94
|
+
const kept = withoutHandlers(groups, isOurs);
|
|
95
|
+
if (kept === null)
|
|
59
96
|
return false;
|
|
60
|
-
|
|
97
|
+
if (kept.length > 0)
|
|
98
|
+
hooks[event] = kept;
|
|
99
|
+
else
|
|
100
|
+
delete hooks[event];
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/** Every command string in `groups`; a group or handler of an unexpected shape adds none. */
|
|
104
|
+
function handlerCommands(groups) {
|
|
105
|
+
return groups.flatMap((group) => (isJsonObject(group) && Array.isArray(group.hooks) ? group.hooks.flatMap((h) => handlerCommand(h) ?? []) : []));
|
|
106
|
+
}
|
|
107
|
+
/** The loose "already installed?" test, on purpose weaker than isHippoCommand: a launcher or env prefix still counts, and a false hit only skips an append. */
|
|
108
|
+
function hookArrayContains(hookArray, marker) {
|
|
109
|
+
return Array.isArray(hookArray) && handlerCommands(hookArray).some((command) => command.includes(marker));
|
|
61
110
|
}
|
|
62
111
|
function addIncludeRecentToPinnedCommand(command) {
|
|
63
|
-
if (!command
|
|
112
|
+
if (!isHippoCommand(command, HIPPO_PINNED_INJECT_MARKER) || command.includes('--include-recent'))
|
|
64
113
|
return command;
|
|
65
114
|
return command.includes(' --format ')
|
|
66
115
|
? command.replace(' --format ', ' --include-recent 5 --format ')
|
|
@@ -90,19 +139,6 @@ function migratePinnedInjectRecentCommands(hookArray) {
|
|
|
90
139
|
}
|
|
91
140
|
return changed;
|
|
92
141
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Returns true when `hooks.SessionEnd` still contains either of the legacy
|
|
95
|
-
* split entries (bare `hippo sleep` / `hippo capture --last-session`)
|
|
96
|
-
* without the current consolidated `hippo session-end` entry.
|
|
97
|
-
*/
|
|
98
|
-
function hasLegacySplitSessionEnd(hookArray) {
|
|
99
|
-
if (!Array.isArray(hookArray))
|
|
100
|
-
return false;
|
|
101
|
-
const serialized = JSON.stringify(hookArray);
|
|
102
|
-
const hasSleep = serialized.includes(HIPPO_SLEEP_MARKER);
|
|
103
|
-
const hasCapture = serialized.includes(HIPPO_CAPTURE_MARKER);
|
|
104
|
-
return (hasSleep || hasCapture) && !serialized.includes(HIPPO_SESSION_END_MARKER);
|
|
105
|
-
}
|
|
106
142
|
function nothingInstalled(target, settingsPath) {
|
|
107
143
|
return {
|
|
108
144
|
target,
|
|
@@ -125,18 +161,23 @@ function nothingInstalled(target, settingsPath) {
|
|
|
125
161
|
function codexCommandHook(command, timeout) {
|
|
126
162
|
return { type: 'command', command, commandWindows: command.replace(/^hippo /, 'hippo.cmd '), timeout };
|
|
127
163
|
}
|
|
164
|
+
/** Assigns `settings.hooks = {}` when it is absent, then returns it if it is an object whose `events`, where present, are arrays; null when appending would overwrite anything else. */
|
|
165
|
+
function ensureHooksObject(settings, events) {
|
|
166
|
+
if (settings.hooks === undefined)
|
|
167
|
+
settings.hooks = {};
|
|
168
|
+
const hooks = settings.hooks;
|
|
169
|
+
if (!isJsonObject(hooks) || events.some((e) => hooks[e] !== undefined && !Array.isArray(hooks[e])))
|
|
170
|
+
return null;
|
|
171
|
+
return hooks;
|
|
172
|
+
}
|
|
128
173
|
/** Codex keys trust to each hook's position and hash and re-asks for a changed one, so hippo only appends and never edits an entry. */
|
|
129
174
|
function installCodexHooks(settingsPath, settings) {
|
|
130
175
|
const result = nothingInstalled('codex', settingsPath);
|
|
131
176
|
if (!isJsonObject(settings))
|
|
132
177
|
return { ...result, invalidJson: true };
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const hooks = settings.hooks;
|
|
136
|
-
const events = ['UserPromptSubmit', 'SessionStart'];
|
|
137
|
-
if (!isJsonObject(hooks) || events.some((e) => hooks[e] !== undefined && !Array.isArray(hooks[e]))) {
|
|
178
|
+
const hooks = ensureHooksObject(settings, ['UserPromptSubmit', 'SessionStart']);
|
|
179
|
+
if (hooks === null)
|
|
138
180
|
return { ...result, invalidJson: true };
|
|
139
|
-
}
|
|
140
181
|
const append = (event, marker, group) => {
|
|
141
182
|
const groups = hooks[event];
|
|
142
183
|
if (hookArrayContains(groups, marker))
|
|
@@ -151,11 +192,11 @@ function installCodexHooks(settingsPath, settings) {
|
|
|
151
192
|
matcher: 'compact',
|
|
152
193
|
hooks: [codexCommandHook(HIPPO_COMPACT_RESUME_MARKER, 10)],
|
|
153
194
|
});
|
|
154
|
-
if (installedUserPromptSubmit || installedCompactResume)
|
|
155
|
-
|
|
156
|
-
}
|
|
195
|
+
if (installedUserPromptSubmit || installedCompactResume)
|
|
196
|
+
writeSettingsFile(settingsPath, settings);
|
|
157
197
|
return { ...result, installedUserPromptSubmit, installedCompactResume };
|
|
158
198
|
}
|
|
199
|
+
/** Adds hippo's hooks to `target`'s settings file and migrates older hippo entries; a file that is not JSON stays untouched. */
|
|
159
200
|
export function installJsonHooks(target) {
|
|
160
201
|
const { settings: settingsPath, logFile } = resolveJsonHookPaths(target);
|
|
161
202
|
const dir = path.dirname(settingsPath);
|
|
@@ -164,7 +205,7 @@ export function installJsonHooks(target) {
|
|
|
164
205
|
let settings = {};
|
|
165
206
|
if (fs.existsSync(settingsPath)) {
|
|
166
207
|
try {
|
|
167
|
-
settings =
|
|
208
|
+
settings = readJsonFile(settingsPath);
|
|
168
209
|
}
|
|
169
210
|
catch {
|
|
170
211
|
return { ...nothingInstalled(target, settingsPath), invalidJson: true };
|
|
@@ -172,35 +213,129 @@ export function installJsonHooks(target) {
|
|
|
172
213
|
}
|
|
173
214
|
if (target === 'codex')
|
|
174
215
|
return installCodexHooks(settingsPath, settings);
|
|
216
|
+
if (!isJsonObject(settings))
|
|
217
|
+
return { ...nothingInstalled(target, settingsPath), invalidJson: true };
|
|
175
218
|
return installClaudeCodeHooks(settingsPath, settings, logFile);
|
|
176
219
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
const RENAME_RETRY_MS = 1000;
|
|
221
|
+
const WINDOWS_RENAME_REFUSALS = ['EPERM', 'EACCES', 'EBUSY'];
|
|
222
|
+
// A rename can be refused where an in-place write still works (a bind-mounted file, a read-only folder), so these fall back to one.
|
|
223
|
+
const REPLACE_REFUSALS = ['EBUSY', 'EXDEV', 'EACCES', 'EPERM'];
|
|
224
|
+
// Matches the usual SYMLOOP_MAX, so a link cycle ends in an error instead of a hang.
|
|
225
|
+
const MAX_LINK_HOPS = 40;
|
|
226
|
+
function errnoCode(err) {
|
|
227
|
+
return 'code' in err ? String(err.code) : '';
|
|
228
|
+
}
|
|
229
|
+
/** Windows refuses a rename onto a file another program has open, usually for a moment, so a refusal is retried before it is reported. */
|
|
230
|
+
function renameOnto(tmp, target) {
|
|
231
|
+
const deadline = Date.now() + RENAME_RETRY_MS;
|
|
232
|
+
const idle = new Int32Array(new SharedArrayBuffer(4));
|
|
233
|
+
for (let pause = 10;; pause = Math.min(pause * 2, 200)) {
|
|
234
|
+
try {
|
|
235
|
+
fs.renameSync(tmp, target);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
const transient = process.platform === 'win32' && err instanceof Error && WINDOWS_RENAME_REFUSALS.includes(errnoCode(err));
|
|
240
|
+
const left = deadline - Date.now();
|
|
241
|
+
if (!transient || left <= 0)
|
|
242
|
+
throw err;
|
|
243
|
+
Atomics.wait(idle, 0, 0, Math.min(pause, left));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Where a write to `file` lands: through any symlink, a dangling one included, so a dotfile manager's link survives. */
|
|
248
|
+
function writeTarget(file) {
|
|
249
|
+
let target = file;
|
|
250
|
+
for (let hop = 0; hop < MAX_LINK_HOPS && fs.lstatSync(target, { throwIfNoEntry: false })?.isSymbolicLink(); hop++) {
|
|
251
|
+
target = path.resolve(path.dirname(target), fs.readlinkSync(target));
|
|
252
|
+
}
|
|
253
|
+
return fs.existsSync(target) ? fs.realpathSync(target) : target;
|
|
254
|
+
}
|
|
255
|
+
/** Gives the replacement the old file's exact mode (umask trims the create mode) and, for root, its owner. */
|
|
256
|
+
function keepAccess(tmp, old) {
|
|
257
|
+
fs.chmodSync(tmp, old.mode & 0o777);
|
|
258
|
+
if (process.getuid?.() === 0)
|
|
259
|
+
fs.chownSync(tmp, old.uid, old.gid);
|
|
260
|
+
}
|
|
261
|
+
/** Cleanup must not hide the failure that made it necessary, so a cleanup error is logged and the first error stays the one thrown. */
|
|
262
|
+
function removeTemp(tmp) {
|
|
263
|
+
try {
|
|
264
|
+
fs.rmSync(tmp, { force: true });
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
log.warn(`could not remove the temporary file ${tmp}: ${errorMessage(err)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function replaceViaTemp(target, text, old) {
|
|
271
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
272
|
+
try {
|
|
273
|
+
fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: (old?.mode ?? 0o666) & 0o777 });
|
|
274
|
+
if (old)
|
|
275
|
+
keepAccess(tmp, old);
|
|
276
|
+
renameOnto(tmp, target);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
removeTemp(tmp);
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** The pre-rename write: it truncates first, so it is only the fallback, but it needs just a writable file and keeps hard links together. */
|
|
284
|
+
function writeInPlace(target, text) {
|
|
285
|
+
try {
|
|
286
|
+
fs.writeFileSync(target, text, 'utf8');
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (!(err instanceof Error))
|
|
290
|
+
throw err;
|
|
291
|
+
const reason = errnoCode(err) === 'EBUSY'
|
|
292
|
+
? 'is in use by another program, so hippo could not replace it; close that program and run the command again'
|
|
293
|
+
: `could not be replaced (${errnoCode(err) || err.message})`;
|
|
294
|
+
throw new Error(`${target} ${reason}`, { cause: err });
|
|
184
295
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
296
|
+
}
|
|
297
|
+
/** Swaps the file in with one rename, so a crash mid-write cannot leave a truncated settings.json that Claude Code cannot parse. */
|
|
298
|
+
export function writeSettingsFile(file, settings) {
|
|
299
|
+
const target = writeTarget(file);
|
|
300
|
+
const text = JSON.stringify(settings, null, 2) + '\n';
|
|
301
|
+
const old = fs.statSync(target, { throwIfNoEntry: false });
|
|
302
|
+
if (old === undefined) {
|
|
303
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// A rename replaces a read-only file that the in-place write refused, and Windows would retry that refusal for a second.
|
|
307
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
308
|
+
}
|
|
309
|
+
if (old !== undefined && old.nlink > 1) {
|
|
310
|
+
// A rename gives the file a new inode, which would leave the other hard links on the old content.
|
|
311
|
+
writeInPlace(target, text);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
replaceViaTemp(target, text, old);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (!(err instanceof Error) || !REPLACE_REFUSALS.includes(errnoCode(err)))
|
|
319
|
+
throw err;
|
|
320
|
+
writeInPlace(target, text);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function migrateLegacyClaudeHooks(hooks) {
|
|
324
|
+
const migratedFromStop = stripHandlers(hooks, 'Stop', runsHippo(HIPPO_SLEEP_MARKER));
|
|
325
|
+
// The legacy SessionEnd forms in the file header collapse into `hippo session-end`, but only while that entry is absent.
|
|
190
326
|
let migratedLegacySessionEnd = false;
|
|
191
327
|
let migratedSplitSessionEnd = false;
|
|
192
|
-
if (Array.isArray(hooks.SessionEnd) &&
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
migratedLegacySessionEnd = before > 1;
|
|
328
|
+
if (Array.isArray(hooks.SessionEnd) && !hookArrayContains(hooks.SessionEnd, HIPPO_SESSION_END_MARKER)) {
|
|
329
|
+
const kept = withoutHandlers(hooks.SessionEnd, runsHippo(HIPPO_SLEEP_MARKER, HIPPO_CAPTURE_MARKER));
|
|
330
|
+
if (kept !== null) {
|
|
331
|
+
// Whatever form was removed reports as the split migration; the legacy flag also marks a SessionEnd that held several groups.
|
|
332
|
+
migratedSplitSessionEnd = true;
|
|
333
|
+
migratedLegacySessionEnd = hooks.SessionEnd.length > 1;
|
|
334
|
+
if (kept.length === 0)
|
|
335
|
+
delete hooks.SessionEnd;
|
|
336
|
+
else
|
|
337
|
+
hooks.SessionEnd = kept;
|
|
338
|
+
}
|
|
204
339
|
}
|
|
205
340
|
return { migratedFromStop, migratedLegacySessionEnd, migratedSplitSessionEnd };
|
|
206
341
|
}
|
|
@@ -216,13 +351,24 @@ function appendHookIfMissing(hooks, event, marker, group) {
|
|
|
216
351
|
hooks[event].push(group);
|
|
217
352
|
return true;
|
|
218
353
|
}
|
|
354
|
+
/** Every command hippo has written to Claude Code's settings per event, the legacy forms included; uninstall removes only handlers that run one. */
|
|
355
|
+
const CLAUDE_HOOK_MARKERS = [
|
|
356
|
+
['SessionEnd', [HIPPO_SESSION_END_MARKER, HIPPO_SLEEP_MARKER, HIPPO_CAPTURE_MARKER]],
|
|
357
|
+
['SessionStart', [HIPPO_LAST_SLEEP_MARKER, HIPPO_COMPACT_RESUME_MARKER]],
|
|
358
|
+
['UserPromptSubmit', [HIPPO_PINNED_INJECT_MARKER]],
|
|
359
|
+
['PreCompact', [HIPPO_PRE_COMPACT_MARKER]],
|
|
360
|
+
['PostCompact', [HIPPO_POST_COMPACT_MARKER]],
|
|
361
|
+
['PostToolUseFailure', [HIPPO_CAPTURE_ERROR_MARKER]],
|
|
362
|
+
['Stop', [HIPPO_SLEEP_MARKER]],
|
|
363
|
+
];
|
|
364
|
+
// Stop is only ever migrated away, so install never appends to it and a Stop that is not a list does not block the install.
|
|
365
|
+
const CLAUDE_APPENDED_EVENTS = CLAUDE_HOOK_MARKERS.map(([event]) => event).filter((event) => event !== 'Stop');
|
|
219
366
|
function installClaudeCodeHooks(settingsPath, settings, logFile) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
const hooks = settings.hooks;
|
|
367
|
+
const merge = ensureHooksObject(settings, CLAUDE_APPENDED_EVENTS);
|
|
368
|
+
if (merge === null)
|
|
369
|
+
return { ...nothingInstalled('claude-code', settingsPath), invalidJson: true };
|
|
370
|
+
// SAFETY: ensureHooksObject left every event hippo appends to absent or an array.
|
|
371
|
+
const hooks = merge;
|
|
226
372
|
const migration = migrateLegacyClaudeHooks(hooks);
|
|
227
373
|
const installedSessionEnd = appendHookIfMissing(hooks, 'SessionEnd', HIPPO_SESSION_END_MARKER, claudeCommandGroup(`hippo session-end --log-file "${logFile}"`, 5));
|
|
228
374
|
const installedSessionStart = appendHookIfMissing(hooks, 'SessionStart', HIPPO_LAST_SLEEP_MARKER, claudeCommandGroup(`hippo last-sleep --path "${logFile}"`, 5));
|
|
@@ -266,9 +412,8 @@ function installClaudeCodeHooks(settingsPath, settings, logFile) {
|
|
|
266
412
|
...migration,
|
|
267
413
|
invalidJson: false,
|
|
268
414
|
};
|
|
269
|
-
if (Object.values(result).some((v) => v === true))
|
|
270
|
-
|
|
271
|
-
}
|
|
415
|
+
if (Object.values(result).some((v) => v === true))
|
|
416
|
+
writeSettingsFile(settingsPath, settings);
|
|
272
417
|
return result;
|
|
273
418
|
}
|
|
274
419
|
/** The exact command hippo writes for each Codex event; uninstall removes only these handlers. */
|
|
@@ -279,37 +424,28 @@ const CODEX_HOOK_COMMANDS = [
|
|
|
279
424
|
/** A group loses only hippo's handlers and goes only once empty, so a user's hook beside or like hippo's stays. */
|
|
280
425
|
function uninstallCodexHooks(hooks) {
|
|
281
426
|
let changed = false;
|
|
282
|
-
for (const [event, command] of CODEX_HOOK_COMMANDS)
|
|
283
|
-
|
|
284
|
-
if (!Array.isArray(groups))
|
|
285
|
-
continue;
|
|
286
|
-
let removed = false;
|
|
287
|
-
const kept = groups.flatMap((group) => {
|
|
288
|
-
if (!isJsonObject(group) || !Array.isArray(group.hooks))
|
|
289
|
-
return [group];
|
|
290
|
-
const handlers = group.hooks.filter((h) => !(isJsonObject(h) && h.command === command));
|
|
291
|
-
if (handlers.length === group.hooks.length)
|
|
292
|
-
return [group];
|
|
293
|
-
removed = true;
|
|
294
|
-
return handlers.length > 0 ? [{ ...group, hooks: handlers }] : [];
|
|
295
|
-
});
|
|
296
|
-
if (!removed)
|
|
297
|
-
continue;
|
|
298
|
-
changed = true;
|
|
299
|
-
if (kept.length > 0)
|
|
300
|
-
hooks[event] = kept;
|
|
301
|
-
else
|
|
302
|
-
delete hooks[event];
|
|
303
|
-
}
|
|
427
|
+
for (const [event, command] of CODEX_HOOK_COMMANDS)
|
|
428
|
+
changed = stripHandlers(hooks, event, (c) => c === command) || changed;
|
|
304
429
|
return changed;
|
|
305
430
|
}
|
|
431
|
+
/** One stderr line naming the handlers uninstall kept that still mention a hippo command (`nice hippo sleep`, `pnpm exec hippo ...`), so the user can remove them by hand. */
|
|
432
|
+
function warnKeptHippoHandlers(settingsPath, hooks) {
|
|
433
|
+
const kept = CLAUDE_HOOK_MARKERS.flatMap(([event, markers]) => {
|
|
434
|
+
const groups = hooks[event];
|
|
435
|
+
return Array.isArray(groups) ? handlerCommands(groups).filter((command) => markers.some((m) => command.includes(m))) : [];
|
|
436
|
+
});
|
|
437
|
+
if (kept.length === 0)
|
|
438
|
+
return;
|
|
439
|
+
process.stderr.write(`hippo kept these hook handlers in ${settingsPath} because they do not start with a hippo command; remove any that are hippo's by hand: ${kept.map((c) => JSON.stringify(c)).join(', ')}\n`);
|
|
440
|
+
}
|
|
441
|
+
/** Removes hippo's own hook handlers from `target`'s settings file and keeps every other handler; true when the file changed. */
|
|
306
442
|
export function uninstallJsonHooks(target) {
|
|
307
443
|
const { settings: settingsPath } = resolveJsonHookPaths(target);
|
|
308
444
|
if (!fs.existsSync(settingsPath))
|
|
309
445
|
return false;
|
|
310
446
|
let settings;
|
|
311
447
|
try {
|
|
312
|
-
settings =
|
|
448
|
+
settings = readJsonFile(settingsPath);
|
|
313
449
|
}
|
|
314
450
|
catch {
|
|
315
451
|
// Never rewrite a settings file we cannot parse; report nothing uninstalled.
|
|
@@ -317,41 +453,34 @@ export function uninstallJsonHooks(target) {
|
|
|
317
453
|
}
|
|
318
454
|
if (!isJsonObject(settings) || !isJsonObject(settings.hooks))
|
|
319
455
|
return false;
|
|
320
|
-
const
|
|
456
|
+
const hooks = settings.hooks;
|
|
457
|
+
const changed = target === 'codex' ? uninstallCodexHooks(hooks) : uninstallClaudeCodeHooks(hooks);
|
|
458
|
+
if (target === 'claude-code')
|
|
459
|
+
warnKeptHippoHandlers(settingsPath, hooks);
|
|
321
460
|
if (!changed)
|
|
322
461
|
return false;
|
|
323
|
-
if (Object.keys(
|
|
462
|
+
if (Object.keys(hooks).length === 0)
|
|
324
463
|
delete settings.hooks;
|
|
325
|
-
|
|
464
|
+
writeSettingsFile(settingsPath, settings);
|
|
326
465
|
return true;
|
|
327
466
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
PreCompact: [HIPPO_PRE_COMPACT_MARKER],
|
|
340
|
-
PostCompact: [HIPPO_POST_COMPACT_MARKER],
|
|
341
|
-
PostToolUseFailure: [HIPPO_CAPTURE_ERROR_MARKER],
|
|
342
|
-
Stop: [HIPPO_SLEEP_MARKER],
|
|
343
|
-
};
|
|
344
|
-
for (const [key, markers] of Object.entries(markersByKey)) {
|
|
345
|
-
if (!Array.isArray(hooks[key]))
|
|
346
|
-
continue;
|
|
347
|
-
const before = hooks[key].length;
|
|
348
|
-
hooks[key] = hooks[key].filter((entry) => !markers.some((m) => JSON.stringify(entry).includes(m)));
|
|
349
|
-
if (hooks[key].length !== before) {
|
|
350
|
-
changed = true;
|
|
351
|
-
if (hooks[key].length === 0)
|
|
352
|
-
delete hooks[key];
|
|
353
|
-
}
|
|
467
|
+
/** Whether uninstall can edit settings.json; `invalidJson` marks a file that exists but is not JSON hippo can edit, which `uninstallJsonHooks` reports only as false. */
|
|
468
|
+
export function checkUninstallable(target) {
|
|
469
|
+
const { settings: settingsPath } = resolveJsonHookPaths(target);
|
|
470
|
+
if (!fs.existsSync(settingsPath))
|
|
471
|
+
return { settingsPath, invalidJson: false };
|
|
472
|
+
try {
|
|
473
|
+
const settings = readJsonFile(settingsPath);
|
|
474
|
+
return { settingsPath, invalidJson: !isJsonObject(settings) || (settings.hooks !== undefined && !isJsonObject(settings.hooks)) };
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return { settingsPath, invalidJson: true };
|
|
354
478
|
}
|
|
479
|
+
}
|
|
480
|
+
function uninstallClaudeCodeHooks(hooks) {
|
|
481
|
+
let changed = false;
|
|
482
|
+
for (const [event, markers] of CLAUDE_HOOK_MARKERS)
|
|
483
|
+
changed = stripHandlers(hooks, event, runsHippo(...markers)) || changed;
|
|
355
484
|
return changed;
|
|
356
485
|
}
|
|
357
486
|
//# sourceMappingURL=json-hooks.js.map
|
package/dist/hooks/opencode.js
CHANGED
|
@@ -89,7 +89,7 @@ function resolveOpencodeConfigPath() {
|
|
|
89
89
|
* must NOT lose the user-authored commands. The migration filters the inner
|
|
90
90
|
* array per-hook, then drops the entry only when its inner array is empty.
|
|
91
91
|
*/
|
|
92
|
-
const HIPPO_OWNED_COMMAND_RE = /^\s*hippo\s+(session-end|last-sleep|sleep|capture|context)
|
|
92
|
+
const HIPPO_OWNED_COMMAND_RE = /^\s*hippo\s+(session-end|last-sleep|sleep|capture|context)(?=\s|$)/;
|
|
93
93
|
function hookIsHippoOwned(hook) {
|
|
94
94
|
if (!isJsonObject(hook))
|
|
95
95
|
return false;
|
package/dist/hooks/shared.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare const HIPPO_POST_COMPACT_MARKER = "hippo post-compact";
|
|
|
24
24
|
export declare function homeDir(): string;
|
|
25
25
|
/** Codex's config folder: $CODEX_HOME, else ~/.codex, as the Codex hooks docs describe. */
|
|
26
26
|
export declare function codexHomeDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
27
|
+
/** Claude Code's config folder, where it reads settings.json: $CLAUDE_CONFIG_DIR when set and non-empty, else ~/.claude under os.homedir(), as Claude Code does (a HOME that differs from the profile must not move it). */
|
|
28
|
+
export declare function claudeConfigDir(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
27
29
|
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
28
30
|
export declare function isCodexPresent(home?: string): boolean;
|
|
29
31
|
/** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
|
package/dist/hooks/shared.js
CHANGED
|
@@ -24,6 +24,10 @@ export function homeDir() {
|
|
|
24
24
|
export function codexHomeDir(home = homeDir(), env = processEnv()) {
|
|
25
25
|
return env.CODEX_HOME || path.join(home, '.codex');
|
|
26
26
|
}
|
|
27
|
+
/** Claude Code's config folder, where it reads settings.json: $CLAUDE_CONFIG_DIR when set and non-empty, else ~/.claude under os.homedir(), as Claude Code does (a HOME that differs from the profile must not move it). */
|
|
28
|
+
export function claudeConfigDir(home = os.homedir(), env = processEnv()) {
|
|
29
|
+
return env.CLAUDE_CONFIG_DIR || path.join(home, '.claude');
|
|
30
|
+
}
|
|
27
31
|
/** Codex counts as installed only when its config folder exists: Codex itself refuses a CODEX_HOME that is not a folder. */
|
|
28
32
|
export function isCodexPresent(home = homeDir()) {
|
|
29
33
|
return fs.statSync(codexHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
@@ -58,8 +62,9 @@ export function ensureDir(dir) {
|
|
|
58
62
|
export function detectInstalledTools() {
|
|
59
63
|
const home = homeDir();
|
|
60
64
|
const exists = (...parts) => fs.existsSync(path.join(home, ...parts));
|
|
65
|
+
const claudeDir = claudeConfigDir();
|
|
61
66
|
return [
|
|
62
|
-
{ name: 'claude-code', configDir:
|
|
67
|
+
{ name: 'claude-code', configDir: claudeDir, detected: fs.existsSync(claudeDir), kind: 'json-hook' },
|
|
63
68
|
{ name: 'opencode', configDir: '~/.config/opencode', detected: exists('.config', 'opencode'), kind: 'plugin', notes: 'installs a TS plugin at ~/.config/opencode/plugins/hippo.ts' },
|
|
64
69
|
{ name: 'openclaw', configDir: '~/.openclaw', detected: exists('.openclaw'), kind: 'plugin', notes: 'install via `openclaw plugins install hippo-memory`' },
|
|
65
70
|
{ name: 'codex', configDir: '~/.codex', detected: isCodexPresent(home), kind: 'wrapper', notes: 'memory hooks in hooks.json, and wraps the detected codex launcher for session-end consolidation' },
|
package/dist/http-util.d.ts
CHANGED
|
@@ -8,10 +8,15 @@ export declare const JSON_HEADERS: {
|
|
|
8
8
|
};
|
|
9
9
|
export declare class HttpError extends Error {
|
|
10
10
|
status: number;
|
|
11
|
-
|
|
11
|
+
/** Sent as the Retry-After header, so a refused caller knows when to come back. */
|
|
12
|
+
readonly retryAfterSec: number | undefined;
|
|
13
|
+
constructor(status: number, message: string, retryAfterSec?: number);
|
|
12
14
|
}
|
|
13
15
|
export declare class BodyTooLargeError extends Error {
|
|
14
16
|
}
|
|
17
|
+
/** The body did not arrive in time. Its own class so mapApiError answers 408 and the server drops the socket. */
|
|
18
|
+
export declare class BodyTimeoutError extends Error {
|
|
19
|
+
}
|
|
15
20
|
/** The status and client-facing message for one failed request. */
|
|
16
21
|
export interface ApiErrorReply {
|
|
17
22
|
status: number;
|
|
@@ -20,13 +25,17 @@ export interface ApiErrorReply {
|
|
|
20
25
|
export declare const INTERNAL_ERROR_MESSAGE = "internal server error";
|
|
21
26
|
/** Maps by class so rewording a message never moves a status; an untyped error is a 500 whose text stays in the server log. */
|
|
22
27
|
export declare function mapApiError<E>(err: E): ApiErrorReply;
|
|
28
|
+
/** Serialises before the head goes out, so a body that is not JSON still reaches the caller's error reply. */
|
|
23
29
|
export declare function sendJson<T>(res: ServerResponse, status: number, body: T): void;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
export interface ReadBodyOpts {
|
|
31
|
+
/** Defaults to 1 MB. */
|
|
32
|
+
maxBytes?: number;
|
|
33
|
+
/** Fails with BodyTimeoutError when the whole body has not arrived by then; unset waits for as long as the socket stays open. */
|
|
34
|
+
deadlineMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/** The body as text, refused mid-stream past maxBytes and past deadlineMs, so an oversized or slow sender cannot tie up the server. */
|
|
37
|
+
export declare function readBody(req: IncomingMessage, { maxBytes, deadlineMs }?: ReadBodyOpts): Promise<string>;
|
|
38
|
+
export declare function closeAfterReply(req: IncomingMessage): void;
|
|
30
39
|
export declare const LOOPBACK_HOST_HEADER: RegExp;
|
|
31
40
|
/** A browser request sent by another site. Non-browser clients send neither header and pass. */
|
|
32
41
|
export declare function isCrossSite(req: IncomingMessage): boolean;
|