hippo-memory 1.66.0 → 1.67.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 -0
- package/dist/api/auth.d.ts +11 -2
- package/dist/api/auth.js +25 -2
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +6 -2
- package/dist/capture/compact.d.ts +2 -0
- package/dist/capture/compact.js +12 -6
- package/dist/capture/copilot-transcript.d.ts +15 -0
- package/dist/capture/copilot-transcript.js +40 -0
- package/dist/capture/failure-reading.js +5 -2
- package/dist/capture/transcript.js +7 -1
- package/dist/capture/working-state.js +14 -3
- package/dist/capture-contract.d.ts +4 -2
- package/dist/capture-contract.js +3 -3
- package/dist/cli/context.js +11 -8
- package/dist/cli/hook-blocks.d.ts +2 -0
- package/dist/cli/hook-blocks.js +6 -0
- package/dist/cli/session-hooks.d.ts +1 -1
- package/dist/cli/session-hooks.js +28 -16
- package/dist/cli/setup.js +66 -7
- package/dist/cli/shared.d.ts +5 -0
- package/dist/cli/shared.js +22 -3
- package/dist/cli/usage.js +20 -9
- package/dist/db/sqlite-blocked.js +1 -1
- package/dist/delivery-recorder.d.ts +3 -1
- package/dist/delivery-recorder.js +1 -1
- package/dist/hooks/copilot.d.ts +45 -0
- package/dist/hooks/copilot.js +187 -0
- package/dist/hooks/json-hooks.d.ts +1 -1
- package/dist/hooks/json-hooks.js +41 -2
- package/dist/hooks/shared.d.ts +5 -1
- package/dist/hooks/shared.js +9 -0
- package/dist/mcp/request.d.ts +0 -2
- package/dist/mcp/request.js +16 -11
- package/dist/prompt-hook.d.ts +6 -0
- package/dist/prompt-hook.js +15 -7
- package/dist/search/vector.js +2 -5
- package/dist/server/routes/admin.js +1 -1
- package/dist/server/types.d.ts +5 -4
- package/dist/server.d.ts +5 -5
- package/dist/server.js +13 -14
- package/dist/stdin.d.ts +5 -0
- package/dist/stdin.js +62 -0
- package/dist/store/key-audit.d.ts +6 -0
- package/dist/store/key-audit.js +25 -0
- package/dist/store-port.d.ts +29 -8
- package/dist/store-port.js +28 -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 +1 -1
package/README.md
CHANGED
|
@@ -844,6 +844,8 @@ For Codex, it adds two hooks to `$CODEX_HOME/hooks.json` (else `~/.codex/hooks.j
|
|
|
844
844
|
|
|
845
845
|
**Codex runs a new or changed hook only after you trust it, so open `/hooks` in Codex once and trust both;** `hippo doctor` reminds you. The per-prompt hook was checked against a real Codex request; the compaction hook follows Codex's documented `compact` start source and has not been watched end to end in Codex. Each hook also carries a `commandWindows` form (`hippo.cmd ...`), because Codex runs hooks through PowerShell on Windows, where the execution policy can block npm's `hippo.ps1`. hippo only ever appends these two entries and never rewrites one, since Codex treats a changed command as a new hook to trust. To remove: `hippo hook uninstall codex`, which takes out only hippo's exact commands and leaves every other hook, including one of yours that runs hippo.
|
|
846
846
|
|
|
847
|
+
For GitHub Copilot, once `$COPILOT_HOME` (else `~/.copilot`) exists, `hippo setup` writes hippo's own `hooks/hippo.json`, adds a `hippo` server to `mcp-config.json` and puts a block in `copilot-instructions.md` that asks the agent to call `hippo_recall` and `hippo_remember`. The Copilot CLI, including its sessions inside VS Code, uses all four hooks, the server and the block. VS Code's Local agent runs only the session-start hook and reads neither of the other files, so it gets no pre-compact snapshot or end-of-session capture yet, and its recall tools need hippo added to VS Code's own `mcp.json`. The hooks, that `mcp.json` recipe and how to remove it all: [integrations/copilot.md](integrations/copilot.md).
|
|
848
|
+
|
|
847
849
|
### What the hook adds (Claude Code example)
|
|
848
850
|
|
|
849
851
|
````markdown
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ApiKeyListItem, type ApiKeyListRow } from '../auth.js';
|
|
2
2
|
import type { KeysetPosition } from '../keyset.js';
|
|
3
|
+
import { type HippoStore } from '../store-port.js';
|
|
3
4
|
import type { Context } from './types.js';
|
|
4
5
|
export interface AuthCreateOpts {
|
|
5
6
|
label?: string;
|
|
@@ -64,9 +65,17 @@ export interface AuthRevokeResult {
|
|
|
64
65
|
ok: true;
|
|
65
66
|
revokedAt: string;
|
|
66
67
|
}
|
|
68
|
+
/** A promise when `ctx` carries a store, since every store method is async; today's plain result when it carries none.
|
|
69
|
+
* hippoRoot keeps the second test off TypeScript's weak-type rule, which would fail a ctx that has no store key at all. */
|
|
70
|
+
export type AuthRevokeReply<C extends Context> = C extends {
|
|
71
|
+
readonly store: HippoStore;
|
|
72
|
+
} ? Promise<AuthRevokeResult> : C extends {
|
|
73
|
+
readonly hippoRoot: string;
|
|
74
|
+
readonly store?: undefined;
|
|
75
|
+
} ? AuthRevokeResult : AuthRevokeResult | Promise<AuthRevokeResult>;
|
|
67
76
|
/** Revoke a key in the caller's tenant: a member API key may revoke only itself, a resolver member only the keys it minted.
|
|
68
|
-
*
|
|
69
|
-
export declare function authRevoke(ctx:
|
|
77
|
+
* With `ctx.store`, its keyAudit group revokes and writes the auth_revoke row; hippo.db is opened only when there is no store. */
|
|
78
|
+
export declare function authRevoke<C extends Context>(ctx: C, keyId: string): AuthRevokeReply<C>;
|
|
70
79
|
/**
|
|
71
80
|
* The tenant that owns `keyId`, or undefined for an unknown key. Host admin only: it reads across tenants,
|
|
72
81
|
* so the local CLI can run revoke and grant in the key's own tenant.
|
package/dist/api/auth.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
3
3
|
import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from '../api-errors.js';
|
|
4
4
|
import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
|
|
5
|
-
import { createApiKey, listApiKeyRows, listLiveOwnedKeyIds, revokeApiKey, grantScope, ungrantScope } from '../auth.js';
|
|
5
|
+
import { createApiKey, forgetVerifiedKey, listApiKeyRows, listLiveOwnedKeyIds, revokeApiKey, grantScope, ungrantScope, } from '../auth.js';
|
|
6
6
|
import { DAY_MS } from '../dashboard-snapshot.js';
|
|
7
7
|
import { isRestrictedScope } from '../recall-scope.js';
|
|
8
|
+
import { requireGroup } from '../store-port.js';
|
|
8
9
|
import { selectApiKeyOwner } from '../store/tenant-lookup.js';
|
|
9
10
|
const API_KEY_SUBJECT = 'api_key:';
|
|
10
11
|
/** The key id an API-key actor's subject names, or null for any other actor. */
|
|
@@ -168,11 +169,33 @@ function assertMayRevoke(ctx, keyId, row) {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
/** Revoke a key in the caller's tenant: a member API key may revoke only itself, a resolver member only the keys it minted.
|
|
171
|
-
*
|
|
172
|
+
* With `ctx.store`, its keyAudit group revokes and writes the auth_revoke row; hippo.db is opened only when there is no store. */
|
|
172
173
|
export function authRevoke(ctx, keyId) {
|
|
174
|
+
const reply = ctx.store ? revokeThroughStore(ctx, ctx.store, keyId) : revokeOnHippoDb(ctx, keyId);
|
|
175
|
+
// SAFETY: a C typed with a store gets the promise its path returns; a C whose type hides a runtime store (a Pick of
|
|
176
|
+
// Context) is typed as the union, which a caller has to await anyway.
|
|
177
|
+
return reply;
|
|
178
|
+
}
|
|
179
|
+
function assertMemberKeyRevokesSelf(ctx, keyId) {
|
|
173
180
|
if (ctx.actor.role !== 'admin' && !ctx.actor.viaAuthResolver && keyIdOfSubject(ctx.actor.subject) !== keyId) {
|
|
174
181
|
throw new ForbiddenError('A member key can revoke only itself');
|
|
175
182
|
}
|
|
183
|
+
}
|
|
184
|
+
function keyOwnerOf(record) {
|
|
185
|
+
return record ? { tenantId: record.tenantId, revokedAt: record.revokedAt, role: record.role, ownerSubject: record.ownerSubject ?? null } : undefined;
|
|
186
|
+
}
|
|
187
|
+
/** The store keeps no handle on this process's verified-key cache, so the key leaves it here once the revoke commits. */
|
|
188
|
+
async function revokeThroughStore(ctx, store, keyId) {
|
|
189
|
+
const keyAudit = requireGroup(store, 'keyAudit');
|
|
190
|
+
assertMemberKeyRevokesSelf(ctx, keyId);
|
|
191
|
+
assertMayRevoke(ctx, keyId, keyOwnerOf(await store.findApiKey(keyId)));
|
|
192
|
+
const revokedAt = await keyAudit.revokeApiKey({ tenantId: ctx.tenantId, keyId, actor: ctx.actor.subject, at: new Date().toISOString() });
|
|
193
|
+
forgetVerifiedKey(keyId);
|
|
194
|
+
return { ok: true, revokedAt };
|
|
195
|
+
}
|
|
196
|
+
/** The auth_revoke row carries the KEY ROW's tenant, as cmdAuthRevoke does, and is skipped for an already-revoked key. */
|
|
197
|
+
function revokeOnHippoDb(ctx, keyId) {
|
|
198
|
+
assertMemberKeyRevokesSelf(ctx, keyId);
|
|
176
199
|
const db = openHippoDb(ctx.hippoRoot);
|
|
177
200
|
try {
|
|
178
201
|
const row = selectApiKeyOwner(db, keyId);
|
package/dist/auth.d.ts
CHANGED
|
@@ -84,7 +84,9 @@ export declare class VerifiedKeyCache {
|
|
|
84
84
|
}
|
|
85
85
|
/** Verify a bearer API key against `store`, served at `hippoRoot`; a cache hit skips both scrypt and the store. Null when invalid. A token whose cache entry lapsed re-reads its record without scrypt; a throw from `beforeScrypt` refuses before scrypt runs. */
|
|
86
86
|
export declare function verifyApiKeyCached(hippoRoot: string, plaintext: string, store: HippoStore, beforeScrypt?: () => void): Promise<VerifiedApiKey | null>;
|
|
87
|
-
export declare function revokeApiKey(db: DatabaseSyncLike, keyId: string): void;
|
|
87
|
+
export declare function revokeApiKey(db: DatabaseSyncLike, keyId: string, at?: string): void;
|
|
88
|
+
/** A store other than hippo.db revokes without this process's cache, so its caller drops the key here once the revoke commits. */
|
|
89
|
+
export declare function forgetVerifiedKey(keyId: string): void;
|
|
88
90
|
/** Grant `keyId` read access to one restricted `scope`. Idempotent. */
|
|
89
91
|
export declare function grantScope(db: DatabaseSyncLike, keyId: string, scope: string): void;
|
|
90
92
|
/** Revoke `keyId`'s grant on `scope`. Not an error when no such grant exists. */
|
package/dist/auth.js
CHANGED
|
@@ -187,9 +187,13 @@ export async function verifyApiKeyCached(hippoRoot, plaintext, store, beforeScry
|
|
|
187
187
|
verifiedKeys.set(hippoRoot, keyId, plaintext, found, Date.now());
|
|
188
188
|
return found?.key ?? null;
|
|
189
189
|
}
|
|
190
|
-
export function revokeApiKey(db, keyId) {
|
|
190
|
+
export function revokeApiKey(db, keyId, at = new Date().toISOString()) {
|
|
191
191
|
db.prepare(`UPDATE api_keys SET revoked_at = ? WHERE key_id = ? AND revoked_at IS NULL`)
|
|
192
|
-
.run(
|
|
192
|
+
.run(at, keyId);
|
|
193
|
+
verifiedKeys.delete(keyId);
|
|
194
|
+
}
|
|
195
|
+
/** A store other than hippo.db revokes without this process's cache, so its caller drops the key here once the revoke commits. */
|
|
196
|
+
export function forgetVerifiedKey(keyId) {
|
|
193
197
|
verifiedKeys.delete(keyId);
|
|
194
198
|
}
|
|
195
199
|
/** Grant `keyId` read access to one restricted `scope`. Idempotent. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HookRuntime } from '../capture-contract.js';
|
|
1
2
|
/**
|
|
2
3
|
* Log-forgery guard: messages here interpolate payload-controlled values
|
|
3
4
|
* (transcript paths, session ids). Strip C0 control chars — newlines above
|
|
@@ -11,6 +12,7 @@ export interface PreCompactOptions {
|
|
|
11
12
|
stdinText?: string;
|
|
12
13
|
stdinTimedOut?: boolean;
|
|
13
14
|
logFile?: string;
|
|
15
|
+
runtime?: HookRuntime;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* PreCompact hook entry point. Exit code 2 on PreCompact BLOCKS compaction,
|
package/dist/capture/compact.js
CHANGED
|
@@ -59,7 +59,8 @@ function printPreCompactInstruction(logFile) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
/** Runs the PreCompact producer: records the compaction, asks the summariser for memories, saves a working-state snapshot. Never extracts memories itself; SessionEnd capture owns that. */
|
|
62
|
-
function runPreCompact(hippoRoot,
|
|
62
|
+
function runPreCompact(hippoRoot, options, logFile) {
|
|
63
|
+
const { stdinText, stdinTimedOut = false, runtime = 'claude-code' } = options;
|
|
63
64
|
// The PreCompact hook fires in every Claude Code project, including
|
|
64
65
|
// ones that never ran `hippo init`, so gate before any store-opening call
|
|
65
66
|
// (saveActiveTaskSnapshot etc. call initStore, which would create one).
|
|
@@ -67,19 +68,20 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
67
68
|
appendPreCompactLog(logFile, 'skip: store not initialized');
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
|
-
const receipt = readClaudeCodePreCompact(stdinText, stdinTimedOut);
|
|
71
|
+
const receipt = readClaudeCodePreCompact(stdinText, stdinTimedOut, runtime);
|
|
71
72
|
if (receipt.status !== 'received') {
|
|
72
73
|
appendPreCompactLog(logFile, `skip: ${receipt.reason}`);
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
const { sessionId, transcriptPath: payloadTranscriptPath, cwd: payloadCwd, trigger: payloadTrigger } = receipt.input;
|
|
76
77
|
// The record is the "something saved before every compaction", so it lands even when no snapshot is derivable below.
|
|
78
|
+
// Copilot has no PostCompact hook to close a record, so its compactions get the snapshot alone.
|
|
77
79
|
let recordId = null;
|
|
78
|
-
if (sessionId !== null && sessionId !== '') {
|
|
80
|
+
if (runtime === 'claude-code' && sessionId !== null && sessionId !== '') {
|
|
79
81
|
recordId = recordCompactionStart(hippoRoot, { sessionId, trigger: payloadTrigger, cwd: payloadCwd, transcriptPath: payloadTranscriptPath }, (message) => appendPreCompactLog(logFile, message));
|
|
80
82
|
printPreCompactInstruction(logFile);
|
|
81
83
|
}
|
|
82
|
-
const transcriptPath = resolvePreCompactTranscript(payloadTranscriptPath, stdinText, logFile);
|
|
84
|
+
const transcriptPath = resolvePreCompactTranscript(payloadTranscriptPath, stdinText, logFile, runtime);
|
|
83
85
|
if (!transcriptPath)
|
|
84
86
|
return;
|
|
85
87
|
// Nothing derivable skips the write, so a user-authored active snapshot is never clobbered with junk.
|
|
@@ -89,7 +91,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
89
91
|
saveDerivedSnapshot(hippoRoot, logFile, sessionId, recordId, derived);
|
|
90
92
|
}
|
|
91
93
|
/** The transcript to snapshot, or null after logging why there is none. */
|
|
92
|
-
function resolvePreCompactTranscript(payloadTranscriptPath, stdinText, logFile) {
|
|
94
|
+
function resolvePreCompactTranscript(payloadTranscriptPath, stdinText, logFile, runtime) {
|
|
93
95
|
// A payload transcript_path is EXCLUSIVE: auto-discovery would snapshot a DIFFERENT session's
|
|
94
96
|
// transcript under THIS payload's session_id, so it runs only on a manual invocation (no payload).
|
|
95
97
|
let transcriptPath;
|
|
@@ -102,6 +104,10 @@ function resolvePreCompactTranscript(payloadTranscriptPath, stdinText, logFile)
|
|
|
102
104
|
return null;
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
else if (runtime === 'copilot') {
|
|
108
|
+
// The scan only knows Claude Code's folders, so a manual Copilot run would snapshot a Claude session.
|
|
109
|
+
transcriptPath = null;
|
|
110
|
+
}
|
|
105
111
|
else {
|
|
106
112
|
transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: true });
|
|
107
113
|
}
|
|
@@ -147,7 +153,7 @@ function saveDerivedSnapshot(hippoRoot, logFile, sessionId, recordId, derived) {
|
|
|
147
153
|
export async function cmdPreCompact(hippoRoot, options) {
|
|
148
154
|
const logFile = options.logFile ?? defaultPreCompactLogPath();
|
|
149
155
|
try {
|
|
150
|
-
runPreCompact(hippoRoot, options
|
|
156
|
+
runPreCompact(hippoRoot, options, logFile);
|
|
151
157
|
}
|
|
152
158
|
catch (err) {
|
|
153
159
|
appendPreCompactLog(logFile, `pre-compact failed: ${errorMessage(err)}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The envelope fields a Copilot event-log line is read by; any may be absent. */
|
|
2
|
+
export interface CopilotEventLine {
|
|
3
|
+
type?: unknown;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
agentId?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface CopilotTurn {
|
|
8
|
+
role: 'user' | 'assistant';
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
/** The Copilot CLI log for a session, `<copilotHome>/session-state/<id>/events.jsonl`; null for an unsafe id or a missing file. */
|
|
12
|
+
export declare function copilotTranscriptFor(sessionId: string): string | null;
|
|
13
|
+
/** The turn one Copilot event-log line carries: the text of a human user.message or of an assistant.message; null for any other line, a sub-agent's included. */
|
|
14
|
+
export declare function copilotTurn(line: CopilotEventLine): CopilotTurn | null;
|
|
15
|
+
//# sourceMappingURL=copilot-transcript.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The Copilot event log: one `{type, data, id, timestamp, parentId}` object per line, written by the Copilot CLI and VS Code Local alike.
|
|
2
|
+
// Matched line by line, since the tail readers start mid-file and never see the session.start line.
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { copilotHomeDir } from '../hooks/shared.js';
|
|
6
|
+
import { isObjectLike, isStringValue } from '../capture-contract.js';
|
|
7
|
+
// A session id becomes a folder name, so anything beyond word characters and hyphens could climb out of session-state.
|
|
8
|
+
const SESSION_ID_RE = /^[\w-]+$/;
|
|
9
|
+
// user.message sources that mark text hippo did not get from the human: a hidden skill injection or another agent's prompt.
|
|
10
|
+
const INJECTED_SOURCE_PREFIXES = ['skill-', 'agent-'];
|
|
11
|
+
/** The Copilot CLI log for a session, `<copilotHome>/session-state/<id>/events.jsonl`; null for an unsafe id or a missing file. */
|
|
12
|
+
export function copilotTranscriptFor(sessionId) {
|
|
13
|
+
if (!SESSION_ID_RE.test(sessionId))
|
|
14
|
+
return null;
|
|
15
|
+
const file = path.join(copilotHomeDir(), 'session-state', sessionId, 'events.jsonl');
|
|
16
|
+
return fs.existsSync(file) ? file : null;
|
|
17
|
+
}
|
|
18
|
+
function isSubagentLine(line, data) {
|
|
19
|
+
if (isStringValue(line.agentId) && line.agentId !== '')
|
|
20
|
+
return true;
|
|
21
|
+
return isStringValue(data.parentToolCallId) && data.parentToolCallId !== '';
|
|
22
|
+
}
|
|
23
|
+
function isInjectedUserMessage(data) {
|
|
24
|
+
if (data.isAutopilotContinuation === true)
|
|
25
|
+
return true;
|
|
26
|
+
const source = data.source;
|
|
27
|
+
return isStringValue(source) && INJECTED_SOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
28
|
+
}
|
|
29
|
+
/** The turn one Copilot event-log line carries: the text of a human user.message or of an assistant.message; null for any other line, a sub-agent's included. */
|
|
30
|
+
export function copilotTurn(line) {
|
|
31
|
+
const role = line.type === 'user.message' ? 'user' : line.type === 'assistant.message' ? 'assistant' : null;
|
|
32
|
+
const data = isObjectLike(line.data) ? line.data : null;
|
|
33
|
+
if (role === null || data === null || isSubagentLine(line, data))
|
|
34
|
+
return null;
|
|
35
|
+
if (role === 'user' && isInjectedUserMessage(data))
|
|
36
|
+
return null;
|
|
37
|
+
const text = isStringValue(data.content) ? data.content.trim() : '';
|
|
38
|
+
return text ? { role, text } : null;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=copilot-transcript.js.map
|
|
@@ -14,6 +14,9 @@ const LEADING_CD = /^\s*(?:(?:cd|pushd)\b[^;&|]*(?:&&|\|\||;)\s*)+/;
|
|
|
14
14
|
const NO_MATCH = /\bno (?:matches|files|results) found\b/i;
|
|
15
15
|
/** Shell commands whose exit code 1 means "nothing found" or "differs", not an error. */
|
|
16
16
|
const QUIET_EXIT_1 = /^\s*(?:grep|rg|egrep|fgrep|find|test|\[|diff|cmp|git diff|git grep)\b/;
|
|
17
|
+
// Claude Code's tool names, then the Copilot CLI's and VS Code's; tests/copilot-failure-reading.test.ts cites where each comes from.
|
|
18
|
+
const SEARCH_TOOLS = new Set(['Grep', 'Glob', 'grep', 'glob', 'rg', 'grep_search', 'file_search', 'semantic_search']);
|
|
19
|
+
const SHELL_TOOLS = new Set(['Bash', 'bash', 'powershell', 'run_in_terminal']);
|
|
17
20
|
function isString(v) {
|
|
18
21
|
return v !== undefined && v !== null && v.constructor === String;
|
|
19
22
|
}
|
|
@@ -58,9 +61,9 @@ export function lessonFromFailure(payload, scrub = (text) => text) {
|
|
|
58
61
|
return routine('os-permission');
|
|
59
62
|
if (NO_MATCH.test(error))
|
|
60
63
|
return routine('no-match');
|
|
61
|
-
if (tool
|
|
64
|
+
if (SEARCH_TOOLS.has(tool))
|
|
62
65
|
return routine('search-tool');
|
|
63
|
-
if (tool
|
|
66
|
+
if (SHELL_TOOLS.has(tool) && QUIET_EXIT_1.test(command) && /exit code 1\b/i.test(error))
|
|
64
67
|
return routine('quiet-exit');
|
|
65
68
|
return { text, detail };
|
|
66
69
|
}
|
|
@@ -3,8 +3,9 @@ import { claudeConfigDir } from '../hooks/shared.js';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { isObjectLike, isStringValue } from '../capture-contract.js';
|
|
6
|
+
import { copilotTurn } from './copilot-transcript.js';
|
|
6
7
|
/**
|
|
7
|
-
* Build a compact text summary from a Claude Code / OpenCode JSONL transcript.
|
|
8
|
+
* Build a compact text summary from a Claude Code / OpenCode / Copilot JSONL transcript.
|
|
8
9
|
* Keeps plain user messages and the final chunk of assistant text, drops
|
|
9
10
|
* thinking blocks, tool_use, and tool_result noise. Capture reads the same
|
|
10
11
|
* turns through `sessionTail`, one text per turn.
|
|
@@ -53,6 +54,11 @@ export function collectSessionTurns(jsonl, visit) {
|
|
|
53
54
|
if (!isObjectLike(entry) || !('type' in entry))
|
|
54
55
|
continue;
|
|
55
56
|
visit?.(entry);
|
|
57
|
+
const copilot = copilotTurn(entry);
|
|
58
|
+
if (copilot !== null) {
|
|
59
|
+
turns.push(copilot);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
56
62
|
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
57
63
|
const message = 'message' in entry && isObjectLike(entry.message) ? entry.message : undefined;
|
|
58
64
|
if (!message)
|
|
@@ -5,6 +5,7 @@ import { isObjectLike, isStringValue } from '../capture-contract.js';
|
|
|
5
5
|
import { errorMessage } from '../log.js';
|
|
6
6
|
import { PRE_COMPACT_TAIL_BYTES, readTranscriptTail, truncateCodePointSafe } from '../transcript-tail.js';
|
|
7
7
|
import { humanUserText, summariseTranscript } from './transcript.js';
|
|
8
|
+
import { copilotTurn } from './copilot-transcript.js';
|
|
8
9
|
export const PRE_COMPACT_TASK_CAP = 200;
|
|
9
10
|
export const PRE_COMPACT_SUMMARY_CAP = 2000;
|
|
10
11
|
export const PRE_COMPACT_NEXT_STEP_CAP = 500;
|
|
@@ -48,7 +49,7 @@ export function truncateKeepNewest(text, maxChars) {
|
|
|
48
49
|
start = nl + 1;
|
|
49
50
|
return TRIM_MARKER + text.slice(start);
|
|
50
51
|
}
|
|
51
|
-
/** Most recent plain-text user message in a JSONL tail
|
|
52
|
+
/** Most recent plain-text user message in a JSONL tail, from a Claude Code or a Copilot transcript. */
|
|
52
53
|
function lastPlainUserMessage(jsonl) {
|
|
53
54
|
const lines = jsonl.split('\n').filter((l) => l.trim());
|
|
54
55
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -61,7 +62,12 @@ function lastPlainUserMessage(jsonl) {
|
|
|
61
62
|
}
|
|
62
63
|
if (!isObjectLike(entry))
|
|
63
64
|
continue;
|
|
64
|
-
if (!('type' in entry)
|
|
65
|
+
if (!('type' in entry))
|
|
66
|
+
continue;
|
|
67
|
+
const copilot = copilotTurn(entry);
|
|
68
|
+
if (copilot?.role === 'user')
|
|
69
|
+
return copilot.text;
|
|
70
|
+
if (entry.type !== 'user')
|
|
65
71
|
continue;
|
|
66
72
|
// An isMeta user line holds an earlier compact summary and an isSidechain one is a sub-agent turn; neither is the human's task.
|
|
67
73
|
if (('isMeta' in entry && entry.isMeta === true) || ('isSidechain' in entry && entry.isSidechain === true))
|
|
@@ -88,7 +94,12 @@ function lastAssistantTextBlock(jsonl) {
|
|
|
88
94
|
}
|
|
89
95
|
if (!isObjectLike(entry))
|
|
90
96
|
continue;
|
|
91
|
-
if (!('type' in entry)
|
|
97
|
+
if (!('type' in entry))
|
|
98
|
+
continue;
|
|
99
|
+
const copilot = copilotTurn(entry);
|
|
100
|
+
if (copilot?.role === 'assistant')
|
|
101
|
+
return copilot.text;
|
|
102
|
+
if (entry.type !== 'assistant')
|
|
92
103
|
continue;
|
|
93
104
|
// Same meta/sidechain guard as lastPlainUserMessage: sub-agent turns
|
|
94
105
|
// (isSidechain) are not this session's next step.
|
|
@@ -42,6 +42,8 @@ export interface ProgressCursor {
|
|
|
42
42
|
export declare function isStringValue<T>(value: T): value is T & string;
|
|
43
43
|
/** An object guard that avoids `typeof`; arrays count as objects, as they do for `typeof`. */
|
|
44
44
|
export declare function isObjectLike<T>(value: T): value is T & object;
|
|
45
|
-
/**
|
|
46
|
-
export
|
|
45
|
+
/** The hook runtimes a `--runtime` flag names; absent means Claude Code. */
|
|
46
|
+
export type HookRuntime = 'claude-code' | 'copilot';
|
|
47
|
+
/** Reads a Claude Code PreCompact payload, or a Copilot one after stdin.ts mapped it to the same snake_case keys; only an empty stdin counts as a manual run. */
|
|
48
|
+
export declare function readClaudeCodePreCompact(stdinText: string | undefined, timedOut: boolean, runtime?: HookRuntime): CaptureReceipt;
|
|
47
49
|
//# sourceMappingURL=capture-contract.d.ts.map
|
package/dist/capture-contract.js
CHANGED
|
@@ -8,13 +8,13 @@ export function isStringValue(value) {
|
|
|
8
8
|
export function isObjectLike(value) {
|
|
9
9
|
return value !== null && value instanceof Object;
|
|
10
10
|
}
|
|
11
|
-
/** Reads a Claude Code PreCompact payload; only an empty stdin counts as a manual run. */
|
|
12
|
-
export function readClaudeCodePreCompact(stdinText, timedOut) {
|
|
11
|
+
/** Reads a Claude Code PreCompact payload, or a Copilot one after stdin.ts mapped it to the same snake_case keys; only an empty stdin counts as a manual run. */
|
|
12
|
+
export function readClaudeCodePreCompact(stdinText, timedOut, runtime = 'claude-code') {
|
|
13
13
|
const empty = !stdinText || stdinText.trim() === '';
|
|
14
14
|
if (timedOut && empty) {
|
|
15
15
|
return { status: 'unavailable', reason: 'no PreCompact payload arrived before the stdin wait window closed' };
|
|
16
16
|
}
|
|
17
|
-
const base = { runtime
|
|
17
|
+
const base = { runtime, event: 'pre-compact' };
|
|
18
18
|
if (empty) {
|
|
19
19
|
return { status: 'received', input: { ...base, manual: true, sessionId: null, cwd: null, transcriptPath: null, trigger: null } };
|
|
20
20
|
}
|
package/dist/cli/context.js
CHANGED
|
@@ -7,14 +7,14 @@ import { isGlobalStoreRoot } from '../project-identity.js';
|
|
|
7
7
|
import { autoDetectContext } from '../context-auto.js';
|
|
8
8
|
import { detectScope } from '../scope.js';
|
|
9
9
|
import { ledgerRoot, withLedgerDb } from '../ledger-db.js';
|
|
10
|
-
import {
|
|
10
|
+
import { readHookStdin } from '../stdin.js';
|
|
11
11
|
import * as api from '../api.js';
|
|
12
12
|
import { resolveTenantId } from '../tenant.js';
|
|
13
13
|
import { renderAmbientSummary } from '../ambient.js';
|
|
14
14
|
import { contextBlockLines, contextCost, crossProjectLines, settleTokens } from '../context-render.js';
|
|
15
|
-
import { additionalContextOutput, flushDeliveryRecorder, hasContextData, toRenderItems, } from '../prompt-hook.js';
|
|
15
|
+
import { additionalContextOutput, flushDeliveryRecorder, hasContextData, sessionStartEnvelope, toRenderItems, } from '../prompt-hook.js';
|
|
16
16
|
import { printError } from './output.js';
|
|
17
|
-
import { parseLimitFlag, parseCountFlag, parseBudgetFlag, requireInit, printActiveTaskSnapshot, printSessionEvents, printHandoff, hostSessionId, captureConsole, hookStoreRoot, runHookWithStores, inPilotHoldout, } from './shared.js';
|
|
17
|
+
import { parseLimitFlag, parseCountFlag, parseBudgetFlag, requireInit, printActiveTaskSnapshot, printSessionEvents, printHandoff, hostSessionId, captureConsole, hookStoreRoot, hookRuntime, payloadCwdRoot, runHookWithStores, inPilotHoldout, } from './shared.js';
|
|
18
18
|
export async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
19
19
|
const rec = startDeliveryRecorder(hippoRoot, flags, stdinText);
|
|
20
20
|
// No try/finally: a render throw keeps its own exit code and writes no event.
|
|
@@ -37,6 +37,7 @@ function startDeliveryRecorder(hippoRoot, flags, stdinText) {
|
|
|
37
37
|
tenantId: resolveTenantId({}),
|
|
38
38
|
stdinText,
|
|
39
39
|
envSessionId: hostSessionId(),
|
|
40
|
+
runtime: hookRuntime(flags) === 'copilot' ? 'copilot' : undefined,
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
catch (error) {
|
|
@@ -102,11 +103,12 @@ async function renderContext(hippoRoot, args, flags, stdinText, rec) {
|
|
|
102
103
|
rec?.delivered({ state: 'empty' });
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
|
-
const
|
|
106
|
+
const envelope = format === 'copilot' ? sessionStartEnvelope(stdinText) : undefined;
|
|
107
|
+
const view = { hippoRoot, tenantId: ctx.tenantId, ledgerSessionId, payloadSessionId, pinnedOnly, framing, rec, result, envelope };
|
|
106
108
|
if (format === 'json') {
|
|
107
109
|
renderContextJson(view, query);
|
|
108
110
|
}
|
|
109
|
-
else if (format === 'additional-context') {
|
|
111
|
+
else if (format === 'additional-context' || format === 'copilot') {
|
|
110
112
|
const stdout = additionalContextOutput(view);
|
|
111
113
|
if (stdout)
|
|
112
114
|
process.stdout.write(stdout);
|
|
@@ -145,7 +147,7 @@ function buildContextOpts(flags, input) {
|
|
|
145
147
|
currentSessionId: input.session.currentSessionId,
|
|
146
148
|
prompt: input.session.prompt,
|
|
147
149
|
// JSON is budgeted as the markdown it stands for, so one budget picks the same memories in every format.
|
|
148
|
-
cost: contextCost(input.format === 'additional-context' ? 'additional-context' : 'markdown', input.framing),
|
|
150
|
+
cost: contextCost(input.format === 'additional-context' || input.format === 'copilot' ? 'additional-context' : 'markdown', input.framing),
|
|
149
151
|
deliveryObserver: input.rec ?? undefined,
|
|
150
152
|
};
|
|
151
153
|
}
|
|
@@ -225,7 +227,8 @@ export function printContextMarkdown(items, totalTokens, framing = 'observe', op
|
|
|
225
227
|
}
|
|
226
228
|
export async function handleContext({ hippoRoot, args, flags }) {
|
|
227
229
|
// Bounded, not a TTY guard: the hot stdin path and a manual run share this one command.
|
|
228
|
-
const { text: stdinText } = await
|
|
229
|
-
|
|
230
|
+
const { text: stdinText } = await readHookStdin();
|
|
231
|
+
const root = payloadCwdRoot(hippoRoot, stdinText, hookRuntime(flags));
|
|
232
|
+
await runHookWithStores(() => cmdContext(hookStoreRoot(root), args, flags, stdinText));
|
|
230
233
|
}
|
|
231
234
|
//# sourceMappingURL=context.js.map
|
|
@@ -15,4 +15,6 @@ export declare function hippoBlock(text: string): {
|
|
|
15
15
|
inner: string;
|
|
16
16
|
owner?: string;
|
|
17
17
|
} | null;
|
|
18
|
+
/** `text` with every hippo block and its markers removed, trimmed. */
|
|
19
|
+
export declare function withoutHookBlock(text: string): string;
|
|
18
20
|
//# sourceMappingURL=hook-blocks.d.ts.map
|
package/dist/cli/hook-blocks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The hippo block each agent's instruction file carries, and how init recognises one it wrote earlier.
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { escapeRegex } from '../escape.js';
|
|
3
4
|
export const HOOK_MARKERS = {
|
|
4
5
|
start: '<!-- hippo:start -->',
|
|
5
6
|
end: '<!-- hippo:end -->',
|
|
@@ -230,4 +231,9 @@ export function hippoBlock(text) {
|
|
|
230
231
|
const owner = Object.keys(HOOKS).find((k) => HOOKS[k].content === inner) ?? SHIPPED_HOOK_HASHES.get(createHash('sha256').update(inner).digest('hex'));
|
|
231
232
|
return { start, end, eol: raw.includes('\r\n') ? '\r\n' : '\n', inner, owner };
|
|
232
233
|
}
|
|
234
|
+
/** `text` with every hippo block and its markers removed, trimmed. */
|
|
235
|
+
export function withoutHookBlock(text) {
|
|
236
|
+
const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
|
|
237
|
+
return text.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
238
|
+
}
|
|
233
239
|
//# sourceMappingURL=hook-blocks.js.map
|
|
@@ -20,7 +20,7 @@ export declare function cmdCodexRun(hippoRoot: string, args: string[]): void;
|
|
|
20
20
|
export declare function cmdCodexSessionEndWorker(hippoRoot: string, flags: Record<string, string | boolean | string[]>): Promise<void>;
|
|
21
21
|
export declare function handlePreCompact({ hippoRoot, flags }: CommandContext): Promise<void>;
|
|
22
22
|
export declare function handlePostCompact({ hippoRoot, flags }: CommandContext): Promise<void>;
|
|
23
|
-
export declare function handleCaptureError({ hippoRoot }: CommandContext): Promise<void>;
|
|
23
|
+
export declare function handleCaptureError({ hippoRoot, flags }: CommandContext): Promise<void>;
|
|
24
24
|
export declare function handleCompactResume({ hippoRoot }: CommandContext): Promise<void>;
|
|
25
25
|
export declare function handleCapture({ hippoRoot, flags }: CommandContext): Promise<void>;
|
|
26
26
|
//# sourceMappingURL=session-hooks.d.ts.map
|
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
5
|
import { resolveCodexSessionTranscript } from '../hooks/codex-session.js';
|
|
6
6
|
import { resolveCodexWrapperPaths } from '../hooks/codex-wrapper.js';
|
|
7
|
+
import { resolveJsonHookPaths } from '../hooks/json-hooks.js';
|
|
7
8
|
import { isInitialized } from '../store/open.js';
|
|
8
9
|
import { loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, listSessionEvents, } from '../store/sessions.js';
|
|
9
10
|
import { writeSessionEndHandoff } from '../store/handoffs.js';
|
|
@@ -20,16 +21,17 @@ import { cmdPreCompact, cmdPostCompact } from '../capture/compact.js';
|
|
|
20
21
|
import { transcriptWorkingState } from '../capture/working-state.js';
|
|
21
22
|
import { collectHandoffEvidence } from '../handoff-evidence.js';
|
|
22
23
|
import { resolveLastSessionTranscript } from '../capture/transcript.js';
|
|
24
|
+
import { copilotTranscriptFor } from '../capture/copilot-transcript.js';
|
|
23
25
|
import { truncateCodePointSafe } from '../transcript-tail.js';
|
|
24
26
|
import { COMPACTION_DB_WAIT_MS } from '../compaction-record.js';
|
|
25
27
|
import { COMPACT_RESUME_EVENT_CONTENT_CAP, COMPACT_RESUME_MAX_AGE_MS, compactResumeText } from '../context-render.js';
|
|
26
|
-
import {
|
|
28
|
+
import { readHookStdin } from '../stdin.js';
|
|
27
29
|
import { resolveTenantId } from '../tenant.js';
|
|
28
30
|
import { errorMessage, log } from '../log.js';
|
|
29
31
|
import { withLedgerDb } from '../ledger-db.js';
|
|
30
32
|
import { printError } from './output.js';
|
|
31
33
|
import { cmdLastSleep } from './last-sleep.js';
|
|
32
|
-
import { logSessionEndImport, appendSessionEndCloseLog, resetHookInjection, hookStoreRoot, runHookWithStores, inPilotHoldout, } from './shared.js';
|
|
34
|
+
import { logSessionEndImport, appendSessionEndCloseLog, resetHookInjection, hookStoreRoot, hookRuntime, payloadCwdRoot, runHookWithStores, inPilotHoldout, } from './shared.js';
|
|
33
35
|
/**
|
|
34
36
|
* SessionStart(compact) injector. Prints the active task snapshot + recent
|
|
35
37
|
* session trail so working state that would otherwise be lost to
|
|
@@ -140,10 +142,14 @@ function restoreCompactSnapshot(hippoRoot, payloadSessionId) {
|
|
|
140
142
|
* the next SessionStart reads the log via `hippo last-sleep`.
|
|
141
143
|
*/
|
|
142
144
|
export async function cmdSessionEnd(hippoRoot, flags) {
|
|
143
|
-
const
|
|
145
|
+
const runtime = hookRuntime(flags);
|
|
146
|
+
// Copilot's hook command carries no path, since one quoted into it would need escaping for each shell; the log goes where the hook table used to point.
|
|
147
|
+
const logFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : runtime === 'copilot' ? resolveJsonHookPaths('copilot').logFile : null;
|
|
144
148
|
// Bounded read: extracts transcript_path + session_id for the detached worker's argv.
|
|
145
149
|
let sessionId = null;
|
|
146
|
-
const { text: stdinText } = await
|
|
150
|
+
const { text: stdinText } = await readHookStdin();
|
|
151
|
+
// Before the spawn, since the worker finds its store from the folder it inherits.
|
|
152
|
+
const root = payloadCwdRoot(hippoRoot, stdinText, runtime);
|
|
147
153
|
try {
|
|
148
154
|
if (stdinText && stdinText.trim().startsWith('{')) {
|
|
149
155
|
const payload = JSON.parse(stdinText);
|
|
@@ -156,8 +162,9 @@ export async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
156
162
|
// No stdin, not JSON, or read failure: the snapshot close below will no-op.
|
|
157
163
|
}
|
|
158
164
|
// Resolved here because only this process saw the payload; the worker captures just the path it is handed.
|
|
159
|
-
// Always a hook, so never scan: an empty stdin here is not a manual run.
|
|
160
|
-
const transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: false })
|
|
165
|
+
// Always a hook, so never scan: an empty stdin here is not a manual run. The Copilot CLI's sessionEnd names no transcript, so its log is found by id.
|
|
166
|
+
const transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: false })
|
|
167
|
+
?? (runtime === 'copilot' && sessionId ? copilotTranscriptFor(sessionId) : null);
|
|
161
168
|
const workerArgs = [process.argv[1], '__session-end-worker'];
|
|
162
169
|
if (logFile)
|
|
163
170
|
workerArgs.push('--log-file', logFile);
|
|
@@ -177,11 +184,13 @@ export async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
177
184
|
}
|
|
178
185
|
catch {
|
|
179
186
|
// If spawn fails, run inline as a last resort, handed what the child's argv would have carried.
|
|
187
|
+
if (logFile)
|
|
188
|
+
flags['log-file'] = logFile;
|
|
180
189
|
if (transcriptPath)
|
|
181
190
|
flags['transcript'] = transcriptPath;
|
|
182
191
|
if (sessionId)
|
|
183
192
|
flags['session-id'] = sessionId;
|
|
184
|
-
await cmdSessionEndWorker(
|
|
193
|
+
await cmdSessionEndWorker(root, flags);
|
|
185
194
|
return;
|
|
186
195
|
}
|
|
187
196
|
}
|
|
@@ -493,19 +502,22 @@ export async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
493
502
|
}
|
|
494
503
|
export async function handlePreCompact({ hippoRoot, flags }) {
|
|
495
504
|
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
496
|
-
const { text: stdinText, timedOut: stdinTimedOut } = await
|
|
505
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readHookStdin();
|
|
506
|
+
const runtime = hookRuntime(flags);
|
|
507
|
+
const root = payloadCwdRoot(hippoRoot, stdinText, runtime);
|
|
497
508
|
await runHookWithStores(async () => {
|
|
498
|
-
resetHookInjection(
|
|
499
|
-
await cmdPreCompact(hookStoreRoot(
|
|
509
|
+
resetHookInjection(root, stdinText, null);
|
|
510
|
+
await cmdPreCompact(hookStoreRoot(root), {
|
|
500
511
|
stdinText,
|
|
501
512
|
stdinTimedOut,
|
|
502
513
|
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
514
|
+
runtime,
|
|
503
515
|
});
|
|
504
516
|
});
|
|
505
517
|
}
|
|
506
518
|
export async function handlePostCompact({ hippoRoot, flags }) {
|
|
507
519
|
// PostCompact hook: saves the compaction summary and its memories, then prints one plain line, because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
508
|
-
const { text } = await
|
|
520
|
+
const { text } = await readHookStdin();
|
|
509
521
|
const logFlag = flags['log-file'];
|
|
510
522
|
const store = hookStoreRoot(hippoRoot);
|
|
511
523
|
const line = await runHookWithStores(() => cmdPostCompact(store, {
|
|
@@ -524,12 +536,12 @@ export async function handlePostCompact({ hippoRoot, flags }) {
|
|
|
524
536
|
if (line !== null && line !== undefined)
|
|
525
537
|
console.log(line);
|
|
526
538
|
}
|
|
527
|
-
export async function handleCaptureError({ hippoRoot }) {
|
|
539
|
+
export async function handleCaptureError({ hippoRoot, flags }) {
|
|
528
540
|
// PostToolUseFailure hook: every path exits 0, and nothing is created
|
|
529
541
|
// when no store exists (the hook fires in every directory).
|
|
530
|
-
const { text } = await
|
|
542
|
+
const { text } = await readHookStdin();
|
|
531
543
|
try {
|
|
532
|
-
const root = hookStoreRoot(hippoRoot);
|
|
544
|
+
const root = hookStoreRoot(payloadCwdRoot(hippoRoot, text, hookRuntime(flags)));
|
|
533
545
|
const payload = (text ?? '').trim();
|
|
534
546
|
if (isInitialized(root) && payload) {
|
|
535
547
|
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
@@ -542,7 +554,7 @@ export async function handleCaptureError({ hippoRoot }) {
|
|
|
542
554
|
}
|
|
543
555
|
}
|
|
544
556
|
export async function handleCompactResume({ hippoRoot }) {
|
|
545
|
-
const { text: stdinText, timedOut: stdinTimedOut } = await
|
|
557
|
+
const { text: stdinText, timedOut: stdinTimedOut } = await readHookStdin();
|
|
546
558
|
await runHookWithStores(() => {
|
|
547
559
|
resetHookInjection(hippoRoot, stdinText, 'compact');
|
|
548
560
|
cmdCompactResume(hookStoreRoot(hippoRoot), stdinText, stdinTimedOut);
|
|
@@ -574,7 +586,7 @@ export async function handleCapture({ hippoRoot, flags }) {
|
|
|
574
586
|
// Bounded, and only when last-session has no explicit path: the
|
|
575
587
|
// --stdin source keeps its own blocking read in capture.ts by design.
|
|
576
588
|
const bounded = captureSource === 'last-session' && !transcriptPath
|
|
577
|
-
? await
|
|
589
|
+
? await readHookStdin()
|
|
578
590
|
: { text: undefined, timedOut: false };
|
|
579
591
|
cmdCapture(hippoRoot, {
|
|
580
592
|
source: captureSource,
|