@raidiant/notifai 10.1.0 → 10.1.2
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/CHANGELOG.md +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2268
- package/src/commands-init.ts +0 -878
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -769
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands.test.ts +0 -7828
- package/src/commands.ts +0 -75
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -282
- package/src/program.ts +0 -818
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/opencode-plugin.ts
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { hookHostPlatform, type HookHostPlatform } from './hook-adapter.js'
|
|
3
|
-
import { configHome } from './install-hooks.js'
|
|
4
|
-
import {
|
|
5
|
-
MISSING_LIFECYCLE_GUIDANCE_CONTEXT,
|
|
6
|
-
WORKER_ACTIVATION_CONTEXT,
|
|
7
|
-
} from './session-activation.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The OpenCode adapter.
|
|
11
|
-
*
|
|
12
|
-
* OpenCode's extension point is a TypeScript/JavaScript plugin module, not a
|
|
13
|
-
* command hook, so it needs a different adapter rather than a different path
|
|
14
|
-
* through the existing one. What it is *not* is different logic: the generated
|
|
15
|
-
* plugin shells out to the same `notifai hook <event>` commands Claude Code and
|
|
16
|
-
* Codex invoke, and every decision — presence, the grace window, the reply
|
|
17
|
-
* wait, retirement — stays in one place.
|
|
18
|
-
*
|
|
19
|
-
* The three lifecycle joints map onto the same three events. OpenCode exposes
|
|
20
|
-
* `chat.message` as a direct hook, while session lifecycle notifications arrive
|
|
21
|
-
* through its generic `event` hook:
|
|
22
|
-
*
|
|
23
|
-
* chat.message -> user-prompt-submit (the user is at the keyboard)
|
|
24
|
-
* session.idle -> stop (the turn ended; escalate)
|
|
25
|
-
* session.deleted -> session-end (retire local session state)
|
|
26
|
-
*
|
|
27
|
-
* Permission prompts deliberately stay in OpenCode. The CLI question flow
|
|
28
|
-
* only escalates questions explicitly registered with `notifai ask`.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/** Marker line; also how an install from another checkout is recognised. */
|
|
32
|
-
export const OPENCODE_PLUGIN_MARKER = '// notifai managed opencode plugin'
|
|
33
|
-
|
|
34
|
-
/** Filename we own inside whichever plugins directory is being written. */
|
|
35
|
-
export const OPENCODE_PLUGIN_FILENAME = 'notifai.js'
|
|
36
|
-
|
|
37
|
-
/** Bump when an installed generated file must be rewritten to remain functional. */
|
|
38
|
-
const OPENCODE_ADAPTER_VERSION = 12
|
|
39
|
-
|
|
40
|
-
export function opencodePluginDir(
|
|
41
|
-
global: boolean,
|
|
42
|
-
cwd: string,
|
|
43
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
-
platform: NodeJS.Platform | HookHostPlatform = process.platform,
|
|
45
|
-
): string {
|
|
46
|
-
return global
|
|
47
|
-
? path.join(configHome(env, 'OPENCODE_CONFIG_DIR', '.config/opencode', platform), 'plugins')
|
|
48
|
-
: path.join(cwd, '.opencode', 'plugins')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function opencodePluginPath(
|
|
52
|
-
global: boolean,
|
|
53
|
-
cwd: string,
|
|
54
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
55
|
-
platform: NodeJS.Platform | HookHostPlatform = process.platform,
|
|
56
|
-
): string {
|
|
57
|
-
return path.join(opencodePluginDir(global, cwd, env, platform), OPENCODE_PLUGIN_FILENAME)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface OpencodePluginOptions {
|
|
61
|
-
/** The same stable user-level adapter every command-hook harness invokes. */
|
|
62
|
-
adapterPath: string
|
|
63
|
-
/** Child-process lifetime; covers grace, reply wait, and teardown headroom. */
|
|
64
|
-
timeoutSeconds: number
|
|
65
|
-
platform?: NodeJS.Platform | HookHostPlatform
|
|
66
|
-
/** Registered Node used to interpret the Windows adapter. Ignored on POSIX. */
|
|
67
|
-
nodePath?: string
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The generated module.
|
|
72
|
-
*
|
|
73
|
-
* Written as a string rather than compiled from a source file because OpenCode
|
|
74
|
-
* loads a plugin module rather than a command definition. It embeds only the
|
|
75
|
-
* stable user-level adapter path; mutable runtime and CLI targets live behind
|
|
76
|
-
* that adapter exactly as they do for every other harness.
|
|
77
|
-
*
|
|
78
|
-
* Everything it does is build the hook envelope OpenCode's event gives it and
|
|
79
|
-
* hand that to the CLI on stdin. It deliberately fails open: an adapter error
|
|
80
|
-
* never changes OpenCode's own behavior.
|
|
81
|
-
*/
|
|
82
|
-
/**
|
|
83
|
-
* The plugin body, which is the same on every platform except for how it
|
|
84
|
-
* reaches the adapter.
|
|
85
|
-
*
|
|
86
|
-
* It was two near-identical copies — about a hundred and twenty lines each,
|
|
87
|
-
* differing only in the spawn call — so every fix to the plugin had to be made
|
|
88
|
-
* twice or silently apply to one platform.
|
|
89
|
-
*/
|
|
90
|
-
export function opencodePluginSource(options: OpencodePluginOptions): string {
|
|
91
|
-
const { adapterPath, timeoutSeconds } = options
|
|
92
|
-
// Windows cannot execute the adapter script directly, so it goes through the
|
|
93
|
-
// registered Node executable and the console window is suppressed.
|
|
94
|
-
const win32 = hookHostPlatform(options.platform) === 'win32'
|
|
95
|
-
const nodeConstant = win32
|
|
96
|
-
? `const NODE = ${JSON.stringify(options.nodePath ?? process.execPath)}\n`
|
|
97
|
-
: ''
|
|
98
|
-
const spawnArguments = win32
|
|
99
|
-
? 'NODE, [ADAPTER, "hook", event, "--owner", "notifai", "--harness", "opencode"]'
|
|
100
|
-
: 'ADAPTER, ["hook", event, "--owner", "notifai", "--harness", "opencode"]'
|
|
101
|
-
const windowsHide = win32 ? '\n windowsHide: true,' : ''
|
|
102
|
-
return `${OPENCODE_PLUGIN_MARKER}
|
|
103
|
-
// Generated by \`notifai hooks install --harness opencode\`. Edits are lost on
|
|
104
|
-
// the next install; change the CLI instead, which is where the logic lives.
|
|
105
|
-
import { spawn } from "node:child_process"
|
|
106
|
-
|
|
107
|
-
${nodeConstant}const ADAPTER = ${JSON.stringify(adapterPath)}
|
|
108
|
-
const TIMEOUT_MS = ${timeoutSeconds * 1000}
|
|
109
|
-
const ADAPTER_VERSION = ${OPENCODE_ADAPTER_VERSION}
|
|
110
|
-
const MISSING_LIFECYCLE_GUIDANCE_CONTEXT = ${JSON.stringify(MISSING_LIFECYCLE_GUIDANCE_CONTEXT)}
|
|
111
|
-
const WORKER_ACTIVATION_CONTEXT = ${JSON.stringify(WORKER_ACTIVATION_CONTEXT)}
|
|
112
|
-
const PENDING_SESSION_TITLE =
|
|
113
|
-
/^New session(?: *- *[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]+)?Z)?$/i
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Runs \`notifai hook <event>\`, feeding it the same JSON envelope the other
|
|
117
|
-
* harnesses send. Resolves to stdout, or null if anything at all went wrong.
|
|
118
|
-
*/
|
|
119
|
-
function runHook(event, envelope) {
|
|
120
|
-
return new Promise((resolve) => {
|
|
121
|
-
let child
|
|
122
|
-
try {
|
|
123
|
-
child = spawn(${spawnArguments}, {
|
|
124
|
-
env: process.env,
|
|
125
|
-
shell: false,${windowsHide}
|
|
126
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
127
|
-
})
|
|
128
|
-
} catch {
|
|
129
|
-
resolve(null)
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let stdout = ""
|
|
134
|
-
let settled = false
|
|
135
|
-
const finish = (value) => {
|
|
136
|
-
if (settled) return
|
|
137
|
-
settled = true
|
|
138
|
-
clearTimeout(timer)
|
|
139
|
-
resolve(value)
|
|
140
|
-
}
|
|
141
|
-
const timer = setTimeout(() => {
|
|
142
|
-
child.kill("SIGTERM")
|
|
143
|
-
finish(null)
|
|
144
|
-
}, TIMEOUT_MS)
|
|
145
|
-
|
|
146
|
-
child.stdout?.setEncoding("utf-8")
|
|
147
|
-
child.stdout?.on("data", (chunk) => {
|
|
148
|
-
stdout += chunk
|
|
149
|
-
})
|
|
150
|
-
// Hook stderr is diagnostics, not plugin output; discard it here.
|
|
151
|
-
child.stderr?.resume()
|
|
152
|
-
child.on("error", () => finish(null))
|
|
153
|
-
child.on("close", () => finish(stdout))
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
child.stdin?.end(JSON.stringify(envelope))
|
|
157
|
-
} catch {
|
|
158
|
-
finish(null)
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export const NotifAIPlugin = async ({ directory, client }) => {
|
|
164
|
-
const cwd = typeof directory === "string" && directory.length > 0 ? directory : process.cwd()
|
|
165
|
-
const activated = new Set()
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
/** Model-visible activation, independent of CLI setup and delivery state. */
|
|
169
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
170
|
-
const sessionID = input?.sessionID
|
|
171
|
-
if (typeof sessionID !== "string" || sessionID.length === 0) return
|
|
172
|
-
if (activated.has(sessionID)) return
|
|
173
|
-
// OpenCode represents delegated work as child sessions. Missing or
|
|
174
|
-
// malformed relationship data must fail closed as worker context: it is
|
|
175
|
-
// safe to suppress a notification, never safe to let a child duplicate
|
|
176
|
-
// its parent's User-visible request.
|
|
177
|
-
let worker = true
|
|
178
|
-
try {
|
|
179
|
-
const response = await client.session.get({ path: { id: sessionID } })
|
|
180
|
-
const session = response?.data
|
|
181
|
-
if (session !== null && typeof session === "object") {
|
|
182
|
-
worker = typeof session.parentID === "string" && session.parentID.length > 0
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
// Keep the safe worker classification.
|
|
186
|
-
}
|
|
187
|
-
const hookEventName = worker ? "SubagentStart" : "SessionStart"
|
|
188
|
-
const resolved = await runHook(worker ? "subagent-start" : "session-start", {
|
|
189
|
-
session_id: sessionID,
|
|
190
|
-
cwd,
|
|
191
|
-
hook_event_name: hookEventName,
|
|
192
|
-
})
|
|
193
|
-
const context = typeof resolved === "string" && resolved.trim().length > 0
|
|
194
|
-
? resolved.trim()
|
|
195
|
-
: worker ? WORKER_ACTIVATION_CONTEXT : MISSING_LIFECYCLE_GUIDANCE_CONTEXT
|
|
196
|
-
if (output.system.length === 0) {
|
|
197
|
-
output.system.push(context)
|
|
198
|
-
} else {
|
|
199
|
-
output.system[0] = output.system[0] + "\\n\\n" + context
|
|
200
|
-
}
|
|
201
|
-
activated.add(sessionID)
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
/** Exact active-harness identity and first-party title for agent shell commands. */
|
|
205
|
-
"shell.env": async (input, output) => {
|
|
206
|
-
output.env.NOTIFAI_ACTIVE_HARNESS = "opencode"
|
|
207
|
-
const sessionID = input?.sessionID
|
|
208
|
-
if (typeof sessionID === "string" && sessionID.length > 0) {
|
|
209
|
-
output.env.NOTIFAI_ACTIVE_SESSION_ID = sessionID
|
|
210
|
-
try {
|
|
211
|
-
const response = await client.session.get({ path: { id: sessionID } })
|
|
212
|
-
const title = response?.data?.title
|
|
213
|
-
if (typeof title === "string" && title.trim().length > 0) {
|
|
214
|
-
if (PENDING_SESSION_TITLE.test(title.trim())) {
|
|
215
|
-
output.env.NOTIFAI_ACTIVE_SESSION_LABEL_PENDING = "1"
|
|
216
|
-
} else {
|
|
217
|
-
output.env.NOTIFAI_ACTIVE_SESSION_LABEL = title
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} catch {
|
|
221
|
-
// Identity remains useful when this OpenCode build cannot expose a title.
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Presence. The user typing is the signal every harness shares, and it is
|
|
228
|
-
* also what publishes the project -> session pointer \`notifai ask\` reads.
|
|
229
|
-
*/
|
|
230
|
-
/** Publishes the session pointer; the CLI returns no content to inject. */
|
|
231
|
-
"chat.message": async (input) => {
|
|
232
|
-
await runHook("user-prompt-submit", {
|
|
233
|
-
session_id: input?.sessionID ?? input?.info?.sessionID,
|
|
234
|
-
cwd,
|
|
235
|
-
hook_event_name: "UserPromptSubmit",
|
|
236
|
-
})
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
/** Session lifecycle events are delivered through OpenCode's event bus. */
|
|
240
|
-
event: async ({ event }) => {
|
|
241
|
-
if (event?.type === "session.idle") {
|
|
242
|
-
await runHook("stop", {
|
|
243
|
-
session_id: event?.properties?.sessionID,
|
|
244
|
-
cwd,
|
|
245
|
-
hook_event_name: "Stop",
|
|
246
|
-
})
|
|
247
|
-
} else if (event?.type === "session.deleted") {
|
|
248
|
-
await runHook("session-end", {
|
|
249
|
-
session_id: event?.properties?.info?.id,
|
|
250
|
-
cwd,
|
|
251
|
-
hook_event_name: "SessionEnd",
|
|
252
|
-
})
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
`
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** True when a plugin file on disk is one of ours, whoever installed it. */
|
|
261
|
-
export function isOurOpencodePlugin(contents: string): boolean {
|
|
262
|
-
return contents.includes(OPENCODE_PLUGIN_MARKER)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* The interpreter and script a generated plugin will actually run.
|
|
267
|
-
*
|
|
268
|
-
* A plugin is a module rather than a command line, so which *build* it invokes
|
|
269
|
-
* is not visible in the way it is for a settings-file handler — it is baked
|
|
270
|
-
* into the two constants at the top. `doctor` needs it to answer the question
|
|
271
|
-
* it exists to answer ("are two checkouts both installed?"), and without it
|
|
272
|
-
* the plugin's own path stands in for the build and every OpenCode install
|
|
273
|
-
* looks like a second one.
|
|
274
|
-
*
|
|
275
|
-
* Returns null for a plugin we did not generate, or one whose constants have
|
|
276
|
-
* been edited out of recognisable shape.
|
|
277
|
-
*/
|
|
278
|
-
export function opencodePluginTarget(
|
|
279
|
-
contents: string,
|
|
280
|
-
): { adapter: string; current: boolean; timeoutSeconds?: number; nodePath?: string } | null {
|
|
281
|
-
if (!isOurOpencodePlugin(contents)) return null
|
|
282
|
-
const adapter = /^const ADAPTER = (".*")$/m.exec(contents)?.[1]
|
|
283
|
-
const legacyScript = /^const SCRIPT = (".*")$/m.exec(contents)?.[1]
|
|
284
|
-
const node = /^const NODE = (".*")$/m.exec(contents)?.[1]
|
|
285
|
-
const version = Number(/^const ADAPTER_VERSION = (\d+)$/m.exec(contents)?.[1] ?? 1)
|
|
286
|
-
const timeoutMs = Number(/^const TIMEOUT_MS = (\d+)$/m.exec(contents)?.[1] ?? Number.NaN)
|
|
287
|
-
if (adapter === undefined && legacyScript === undefined) return null
|
|
288
|
-
try {
|
|
289
|
-
return {
|
|
290
|
-
adapter: JSON.parse(adapter ?? legacyScript!) as string,
|
|
291
|
-
current: version === OPENCODE_ADAPTER_VERSION,
|
|
292
|
-
...(Number.isFinite(timeoutMs) ? { timeoutSeconds: timeoutMs / 1000 } : {}),
|
|
293
|
-
...(node === undefined ? {} : { nodePath: JSON.parse(node) as string }),
|
|
294
|
-
}
|
|
295
|
-
} catch {
|
|
296
|
-
return null
|
|
297
|
-
}
|
|
298
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { readOrcaSessionTitle, type OrcaCommand } from './orca-session-title.js'
|
|
3
|
-
|
|
4
|
-
const worktreeId = 'repo-123::/worktrees/agent-session-labels'
|
|
5
|
-
|
|
6
|
-
function response(overrides: Record<string, unknown> = {}): string {
|
|
7
|
-
return JSON.stringify({
|
|
8
|
-
ok: true,
|
|
9
|
-
result: {
|
|
10
|
-
worktree: {
|
|
11
|
-
id: worktreeId,
|
|
12
|
-
path: '/worktrees/agent-session-labels',
|
|
13
|
-
displayName: 'Agent Session context labels',
|
|
14
|
-
...overrides,
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('Orca Agent Session title context', () => {
|
|
21
|
-
it('accepts only the User-facing title returned for the exact managed worktree', () => {
|
|
22
|
-
const calls: { executable: string; args: readonly string[] }[] = []
|
|
23
|
-
const command: OrcaCommand = (executable, args) => {
|
|
24
|
-
calls.push({ executable, args })
|
|
25
|
-
return response()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
expect(
|
|
29
|
-
readOrcaSessionTitle({ TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: worktreeId }, command),
|
|
30
|
-
).toBe('Agent Session context labels')
|
|
31
|
-
expect(calls).toEqual([
|
|
32
|
-
{
|
|
33
|
-
executable: 'orca',
|
|
34
|
-
args: ['worktree', 'show', '--worktree', `id:${worktreeId}`, '--json'],
|
|
35
|
-
},
|
|
36
|
-
])
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it.each([
|
|
40
|
-
['command failure', () => null],
|
|
41
|
-
['malformed JSON', () => '{not json}'],
|
|
42
|
-
['failed response', () => JSON.stringify({ ok: false })],
|
|
43
|
-
['different worktree id', () => response({ id: 'repo-123::/worktrees/other' })],
|
|
44
|
-
['different worktree path', () => response({ path: '/private/untrusted' })],
|
|
45
|
-
['blank display name', () => response({ displayName: ' ' })],
|
|
46
|
-
])('leaves the generated fallback exceptional when context is unavailable: %s', (_name, command) => {
|
|
47
|
-
expect(
|
|
48
|
-
readOrcaSessionTitle(
|
|
49
|
-
{ TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: worktreeId },
|
|
50
|
-
command,
|
|
51
|
-
),
|
|
52
|
-
).toBeUndefined()
|
|
53
|
-
})
|
|
54
|
-
})
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
export type OrcaSessionTitleLookup = (env: NodeJS.ProcessEnv) => string | undefined
|
|
5
|
-
export type OrcaCommand = (executable: string, args: readonly string[]) => string | null
|
|
6
|
-
|
|
7
|
-
const runOrcaCommand: OrcaCommand = (executable, args) => {
|
|
8
|
-
try {
|
|
9
|
-
return execFileSync(executable, args, {
|
|
10
|
-
encoding: 'utf8',
|
|
11
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
12
|
-
timeout: 2_000,
|
|
13
|
-
maxBuffer: 512 * 1024,
|
|
14
|
-
windowsHide: true,
|
|
15
|
-
}).trim()
|
|
16
|
-
} catch {
|
|
17
|
-
return null
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function containsControlCharacter(value: string): boolean {
|
|
22
|
-
return Array.from(value).some((character) => {
|
|
23
|
-
const codePoint = character.codePointAt(0) ?? 0
|
|
24
|
-
return codePoint < 32 || codePoint === 127
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function worktreePathFromId(value: string): string | null {
|
|
29
|
-
if (value.length === 0 || value.length > 4_096 || containsControlCharacter(value)) return null
|
|
30
|
-
const separator = value.indexOf('::')
|
|
31
|
-
if (separator <= 0) return null
|
|
32
|
-
const repoId = value.slice(0, separator)
|
|
33
|
-
const worktreePath = value.slice(separator + 2)
|
|
34
|
-
if (!/^[A-Za-z0-9._-]+$/u.test(repoId) || worktreePath.length === 0) return null
|
|
35
|
-
return path.posix.isAbsolute(worktreePath) || path.win32.isAbsolute(worktreePath)
|
|
36
|
-
? worktreePath
|
|
37
|
-
: null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function orcaExecutable(env: NodeJS.ProcessEnv): string | null {
|
|
41
|
-
const configured = env['ORCA_CLI_COMMAND']?.trim()
|
|
42
|
-
if (configured !== undefined && configured.length > 0) {
|
|
43
|
-
return containsControlCharacter(configured) ? null : configured
|
|
44
|
-
}
|
|
45
|
-
return env['ORCA_DEV_REPO_ROOT'] === undefined ? 'orca' : 'orca-dev'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Read the User-facing task name assigned to this exact Orca worktree.
|
|
50
|
-
*
|
|
51
|
-
* The environment contributes only an opaque selector. Orca must return that
|
|
52
|
-
* exact id and its matching path before its display name becomes trusted; no
|
|
53
|
-
* path component or identifier is ever promoted into an Agent Session label.
|
|
54
|
-
*/
|
|
55
|
-
export function readOrcaSessionTitle(
|
|
56
|
-
env: NodeJS.ProcessEnv,
|
|
57
|
-
command: OrcaCommand = runOrcaCommand,
|
|
58
|
-
): string | undefined {
|
|
59
|
-
if (env['TERM_PROGRAM'] !== 'Orca') return undefined
|
|
60
|
-
const worktreeId = env['ORCA_WORKTREE_ID']
|
|
61
|
-
if (worktreeId === undefined) return undefined
|
|
62
|
-
const expectedPath = worktreePathFromId(worktreeId)
|
|
63
|
-
if (expectedPath === null) return undefined
|
|
64
|
-
const executable = orcaExecutable(env)
|
|
65
|
-
if (executable === null) return undefined
|
|
66
|
-
|
|
67
|
-
let output: string | null
|
|
68
|
-
try {
|
|
69
|
-
output = command(executable, [
|
|
70
|
-
'worktree',
|
|
71
|
-
'show',
|
|
72
|
-
'--worktree',
|
|
73
|
-
`id:${worktreeId}`,
|
|
74
|
-
'--json',
|
|
75
|
-
])
|
|
76
|
-
} catch {
|
|
77
|
-
return undefined
|
|
78
|
-
}
|
|
79
|
-
if (output === null || output === '') return undefined
|
|
80
|
-
|
|
81
|
-
let parsed: unknown
|
|
82
|
-
try {
|
|
83
|
-
parsed = JSON.parse(output)
|
|
84
|
-
} catch {
|
|
85
|
-
return undefined
|
|
86
|
-
}
|
|
87
|
-
if (typeof parsed !== 'object' || parsed === null) return undefined
|
|
88
|
-
const response = parsed as {
|
|
89
|
-
ok?: unknown
|
|
90
|
-
result?: { worktree?: { id?: unknown; path?: unknown; displayName?: unknown } }
|
|
91
|
-
}
|
|
92
|
-
const worktree = response.result?.worktree
|
|
93
|
-
if (
|
|
94
|
-
response.ok !== true ||
|
|
95
|
-
worktree?.id !== worktreeId ||
|
|
96
|
-
worktree.path !== expectedPath ||
|
|
97
|
-
typeof worktree.displayName !== 'string' ||
|
|
98
|
-
worktree.displayName.trim().length === 0
|
|
99
|
-
) {
|
|
100
|
-
return undefined
|
|
101
|
-
}
|
|
102
|
-
return worktree.displayName
|
|
103
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
2
|
-
import { appendFileSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { gunzipSync } from 'node:zlib'
|
|
6
|
-
import { describe, expect, it } from 'vitest'
|
|
7
|
-
import { LOG_SCHEMA_VERSION, activeLogPath, createLogger, type LogRecord } from './logging.js'
|
|
8
|
-
import { encodeFeedbackSlice, packFeedbackSlice } from './pack-feedback-slice.js'
|
|
9
|
-
|
|
10
|
-
function sandbox(): NodeJS.ProcessEnv {
|
|
11
|
-
return { XDG_STATE_HOME: mkdtempSync(path.join(os.tmpdir(), 'notifai-feedback-')) }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function writeLog(env: NodeJS.ProcessEnv, records: LogRecord[]): void {
|
|
15
|
-
const file = activeLogPath(env)
|
|
16
|
-
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
17
|
-
writeFileSync(file, `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, {
|
|
18
|
-
mode: 0o600,
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function record(overrides: Partial<LogRecord> & Pick<LogRecord, 'ts'>): LogRecord {
|
|
23
|
-
return {
|
|
24
|
-
v: 1,
|
|
25
|
-
level: 'info',
|
|
26
|
-
event: 'cli.end',
|
|
27
|
-
run: 'r_1',
|
|
28
|
-
cmd: 'send',
|
|
29
|
-
pid: 1,
|
|
30
|
-
...overrides,
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('packFeedbackSlice', () => {
|
|
35
|
-
it('returns an empty result with no gzip when there is no log', () => {
|
|
36
|
-
const result = packFeedbackSlice(sandbox())
|
|
37
|
-
|
|
38
|
-
expect(result.record_count).toBe(0)
|
|
39
|
-
expect(result.records).toEqual([])
|
|
40
|
-
expect(result.jsonl).toBe('')
|
|
41
|
-
expect(result.gzip).toBeUndefined()
|
|
42
|
-
expect(result.uncompressed_bytes).toBe(0)
|
|
43
|
-
expect(result.compressed_bytes).toBe(0)
|
|
44
|
-
expect(result.truncated).toBe(false)
|
|
45
|
-
expect(result.consentSummary.toLowerCase()).toMatch(/credential/)
|
|
46
|
-
expect(result.consentSummary.toLowerCase()).toMatch(/title/)
|
|
47
|
-
expect(result.consentSummary.toLowerCase()).toMatch(/answer/)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('packs records inside the default 24-hour window and drops older ones', () => {
|
|
51
|
-
const env = sandbox()
|
|
52
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
53
|
-
writeLog(env, [
|
|
54
|
-
record({ ts: '2026-08-12T11:00:00.000Z', data: { which: 'too-old' } }),
|
|
55
|
-
record({ ts: '2026-08-12T13:00:00.000Z', data: { which: 'kept' } }),
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
const result = packFeedbackSlice(env, { now })
|
|
59
|
-
|
|
60
|
-
expect(result.record_count).toBe(1)
|
|
61
|
-
expect(result.records[0]?.data).toMatchObject({ which: 'kept' })
|
|
62
|
-
expect(result.since).toBe(Date.parse('2026-08-12T12:00:00.000Z'))
|
|
63
|
-
expect(result.until).toBe(now)
|
|
64
|
-
expect(result.jsonl).toContain('kept')
|
|
65
|
-
expect(result.jsonl).not.toContain('too-old')
|
|
66
|
-
expect(result.gzip).toBeDefined()
|
|
67
|
-
expect(result.uncompressed_bytes).toBe(Buffer.byteLength(result.jsonl))
|
|
68
|
-
expect(result.compressed_bytes).toBe(result.gzip!.length)
|
|
69
|
-
expect(result.truncated).toBe(false)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('gives consent copy that names redacted credentials, titles, answers, window, and size', () => {
|
|
73
|
-
const env = sandbox()
|
|
74
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
75
|
-
writeLog(env, [record({ ts: '2026-08-13T11:00:00.000Z' })])
|
|
76
|
-
|
|
77
|
-
const result = packFeedbackSlice(env, { now })
|
|
78
|
-
const copy = result.consentSummary.toLowerCase()
|
|
79
|
-
|
|
80
|
-
expect(copy).toMatch(/credential/)
|
|
81
|
-
expect(copy).toMatch(/already removed|already redacted|redacted/)
|
|
82
|
-
expect(copy).toMatch(/title/)
|
|
83
|
-
expect(copy).toMatch(/answer/)
|
|
84
|
-
expect(result.consentSummary).toContain('2026-08-12T12:00:00.000Z')
|
|
85
|
-
expect(result.consentSummary).toContain('2026-08-13T12:00:00.000Z')
|
|
86
|
-
expect(result.consentSummary).toContain(String(result.uncompressed_bytes))
|
|
87
|
-
expect(result.consentSummary).toContain(String(result.compressed_bytes))
|
|
88
|
-
expect(result.consentSummary).toContain(String(result.record_count))
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('applies extra log query filters on top of the window', () => {
|
|
92
|
-
const env = sandbox()
|
|
93
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
94
|
-
writeLog(env, [
|
|
95
|
-
record({ ts: '2026-08-13T11:00:00.000Z', event: 'cli.end', data: { which: 'end' } }),
|
|
96
|
-
record({ ts: '2026-08-13T11:01:00.000Z', event: 'cli.error', data: { which: 'error' } }),
|
|
97
|
-
])
|
|
98
|
-
|
|
99
|
-
const result = packFeedbackSlice(env, { now, event: ['cli.error'] })
|
|
100
|
-
|
|
101
|
-
expect(result.record_count).toBe(1)
|
|
102
|
-
expect(result.records[0]?.event).toBe('cli.error')
|
|
103
|
-
expect(result.records[0]?.data).toMatchObject({ which: 'error' })
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('drops oldest records until the uncompressed JSONL cap holds', () => {
|
|
107
|
-
const env = sandbox()
|
|
108
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
109
|
-
const start = Date.parse('2026-08-13T10:00:00.000Z')
|
|
110
|
-
const pad = 'x'.repeat(2000)
|
|
111
|
-
writeLog(
|
|
112
|
-
env,
|
|
113
|
-
Array.from({ length: 200 }, (_, sequence) =>
|
|
114
|
-
record({
|
|
115
|
-
ts: new Date(start + sequence * 1000).toISOString(),
|
|
116
|
-
data: { sequence, pad },
|
|
117
|
-
}),
|
|
118
|
-
),
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
const result = packFeedbackSlice(env, { now })
|
|
122
|
-
const sequences = result.records.map((entry) => entry.data?.['sequence'])
|
|
123
|
-
|
|
124
|
-
expect(result.uncompressed_bytes).toBeLessThanOrEqual(256 * 1024)
|
|
125
|
-
expect(result.compressed_bytes).toBeLessThanOrEqual(128 * 1024)
|
|
126
|
-
expect(result.truncated).toBe(true)
|
|
127
|
-
expect(result.record_count).toBeGreaterThan(0)
|
|
128
|
-
expect(sequences[0]).toBeGreaterThan(0)
|
|
129
|
-
expect(sequences.at(-1)).toBe(199)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('drops oldest records until the gzip cap holds', () => {
|
|
133
|
-
const env = sandbox()
|
|
134
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
135
|
-
const start = Date.parse('2026-08-13T10:00:00.000Z')
|
|
136
|
-
const seeded = Array.from({ length: 80 }, (_, sequence) =>
|
|
137
|
-
record({
|
|
138
|
-
ts: new Date(start + sequence * 1000).toISOString(),
|
|
139
|
-
data: { sequence, pad: randomBytes(1800).toString('base64') },
|
|
140
|
-
}),
|
|
141
|
-
)
|
|
142
|
-
writeLog(env, seeded)
|
|
143
|
-
|
|
144
|
-
const result = packFeedbackSlice(env, { now })
|
|
145
|
-
const sequences = result.records.map((entry) => entry.data?.['sequence'])
|
|
146
|
-
|
|
147
|
-
expect(result.uncompressed_bytes).toBeLessThanOrEqual(256 * 1024)
|
|
148
|
-
expect(result.compressed_bytes).toBeLessThanOrEqual(128 * 1024)
|
|
149
|
-
expect(result.truncated).toBe(true)
|
|
150
|
-
expect(result.record_count).toBeGreaterThan(0)
|
|
151
|
-
expect(result.record_count).toBeLessThan(seeded.length)
|
|
152
|
-
expect(sequences[0]).toBeGreaterThan(0)
|
|
153
|
-
expect(sequences.at(-1)).toBe(79)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('encodes the gzip slice as a gzip+base64 wire payload', () => {
|
|
157
|
-
const env = sandbox()
|
|
158
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
159
|
-
writeLog(env, [record({ ts: '2026-08-13T11:00:00.000Z', data: { which: 'wire' } })])
|
|
160
|
-
|
|
161
|
-
const packed = packFeedbackSlice(env, { now })
|
|
162
|
-
const wire = encodeFeedbackSlice(packed)
|
|
163
|
-
|
|
164
|
-
expect(wire).toEqual({
|
|
165
|
-
encoding: 'gzip+base64',
|
|
166
|
-
bytes: packed.gzip!.toString('base64'),
|
|
167
|
-
uncompressed_bytes: packed.uncompressed_bytes,
|
|
168
|
-
compressed_bytes: packed.compressed_bytes,
|
|
169
|
-
record_count: packed.record_count,
|
|
170
|
-
truncated: false,
|
|
171
|
-
since: '2026-08-12T12:00:00.000Z',
|
|
172
|
-
until: '2026-08-13T12:00:00.000Z',
|
|
173
|
-
schema_version: packed.schema_version,
|
|
174
|
-
})
|
|
175
|
-
expect(gunzipSync(Buffer.from(wire!.bytes, 'base64')).toString('utf8')).toBe(packed.jsonl)
|
|
176
|
-
expect(encodeFeedbackSlice(packFeedbackSlice(sandbox()))).toBeUndefined()
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('honours an explicit since instead of the 24-hour default', () => {
|
|
180
|
-
const env = sandbox()
|
|
181
|
-
const now = Date.parse('2026-08-13T12:00:00.000Z')
|
|
182
|
-
writeLog(env, [
|
|
183
|
-
record({ ts: '2026-08-10T12:00:00.000Z', data: { which: 'old' } }),
|
|
184
|
-
record({ ts: '2026-08-13T11:00:00.000Z', data: { which: 'new' } }),
|
|
185
|
-
])
|
|
186
|
-
|
|
187
|
-
const result = packFeedbackSlice(env, {
|
|
188
|
-
now,
|
|
189
|
-
since: Date.parse('2026-08-10T00:00:00.000Z'),
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
expect(result.records.map((entry) => entry.data?.['which'])).toEqual(['old', 'new'])
|
|
193
|
-
expect(result.since).toBe(Date.parse('2026-08-10T00:00:00.000Z'))
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('packs records written by the logger and skips a damaged line', () => {
|
|
197
|
-
const env = sandbox()
|
|
198
|
-
const logger = createLogger({ env, now: () => Date.parse('2026-08-13T11:30:00.000Z') })
|
|
199
|
-
logger.info('cli.end', { which: 'first' })
|
|
200
|
-
appendFileSync(activeLogPath(env), 'not json at all\n')
|
|
201
|
-
createLogger({ env, now: () => Date.parse('2026-08-13T11:31:00.000Z') }).info('cli.end', {
|
|
202
|
-
which: 'second',
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
const result = packFeedbackSlice(env, { now: Date.parse('2026-08-13T12:00:00.000Z') })
|
|
206
|
-
|
|
207
|
-
expect(result.schema_version).toBe(LOG_SCHEMA_VERSION)
|
|
208
|
-
expect(result.record_count).toBe(2)
|
|
209
|
-
expect(result.records.map((entry) => entry.data?.['which'])).toEqual(['first', 'second'])
|
|
210
|
-
expect(result.truncated).toBe(false)
|
|
211
|
-
})
|
|
212
|
-
})
|