runwork 0.11.0 → 0.13.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/dist/agents/__tests__/intro-skill.test.js +32 -0
- package/dist/agents/claude-code.d.ts +21 -0
- package/dist/agents/claude-code.js +58 -2
- package/dist/agents/claude-desktop-plugin-tree.d.ts +15 -0
- package/dist/agents/claude-desktop-plugin-tree.js +37 -1
- package/dist/agents/conversation-skills.d.ts +18 -0
- package/dist/agents/conversation-skills.js +229 -0
- package/dist/agents/intro-skill.d.ts +9 -0
- package/dist/agents/intro-skill.js +41 -0
- package/dist/agents/registry-data.d.ts +35 -0
- package/dist/agents/registry-data.js +58 -1
- package/dist/agents/runtime-detection.d.ts +82 -0
- package/dist/agents/runtime-detection.js +271 -0
- package/dist/agents/session-start-hook.d.ts +37 -0
- package/dist/agents/session-start-hook.js +159 -0
- package/dist/agents/types.d.ts +12 -0
- package/dist/api/client.d.ts +47 -0
- package/dist/api/client.js +32 -0
- package/dist/commands/__tests__/setup-persona.test.d.ts +1 -0
- package/dist/commands/__tests__/setup-persona.test.js +31 -0
- package/dist/commands/doctor.js +89 -2
- package/dist/commands/inbox.d.ts +11 -0
- package/dist/commands/inbox.js +60 -0
- package/dist/commands/info.d.ts +1 -1
- package/dist/commands/info.js +7 -2
- package/dist/commands/resume.d.ts +2 -0
- package/dist/commands/resume.js +265 -0
- package/dist/commands/save-convo.d.ts +8 -0
- package/dist/commands/save-convo.js +20 -0
- package/dist/commands/setup.d.ts +7 -0
- package/dist/commands/setup.js +22 -0
- package/dist/commands/share-convo.d.ts +24 -0
- package/dist/commands/share-convo.js +167 -0
- package/dist/commands/sync.js +96 -10
- package/dist/generated/bundled-types.js +33 -33
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.js +8 -0
- package/dist/types.d.ts +13 -0
- package/dist/utils/app-info.d.ts +4 -0
- package/dist/utils/app-info.js +17 -0
- package/package.json +1 -1
|
@@ -31,6 +31,11 @@ const AGENT_REGISTRY = [
|
|
|
31
31
|
instructionFile: { global: '.claude/CLAUDE.md', project: '.claude/CLAUDE.md' },
|
|
32
32
|
mcpConfigKey: 'mcpServers',
|
|
33
33
|
firstClass: true,
|
|
34
|
+
resumeCapability: {
|
|
35
|
+
mode: 'cli-resume',
|
|
36
|
+
nativeBundleFormat: 'claude-jsonl',
|
|
37
|
+
cliResumeCommand: 'claude --resume {uuid}',
|
|
38
|
+
},
|
|
34
39
|
quickStart: {
|
|
35
40
|
launchHint: 'Open your terminal and type `claude`',
|
|
36
41
|
skillCommand: '/skills',
|
|
@@ -64,6 +69,11 @@ const AGENT_REGISTRY = [
|
|
|
64
69
|
},
|
|
65
70
|
mcpConfigKey: 'mcpServers',
|
|
66
71
|
firstClass: true,
|
|
72
|
+
resumeCapability: {
|
|
73
|
+
mode: 'file-drop-only',
|
|
74
|
+
nativeBundleFormat: 'claude-jsonl',
|
|
75
|
+
manualOpenHint: 'Open Claude Desktop, switch to the Cowork tab, and the conversation will appear in your session history.',
|
|
76
|
+
},
|
|
67
77
|
quickStart: {
|
|
68
78
|
launchHint: 'Open Claude Desktop',
|
|
69
79
|
examplePrompts: [
|
|
@@ -197,6 +207,11 @@ const AGENT_REGISTRY = [
|
|
|
197
207
|
skillsPaths: { global: '.codex/skills', project: '.agents/skills' },
|
|
198
208
|
instructionFile: { global: '.codex/instructions.md', project: 'AGENTS.md' },
|
|
199
209
|
firstClass: true,
|
|
210
|
+
resumeCapability: {
|
|
211
|
+
mode: 'file-drop-only',
|
|
212
|
+
nativeBundleFormat: 'codex-rollout',
|
|
213
|
+
manualOpenHint: 'Open Codex Desktop and the conversation will appear in your recent threads list.',
|
|
214
|
+
},
|
|
200
215
|
quickStart: {
|
|
201
216
|
launchHint: 'Open Codex',
|
|
202
217
|
examplePrompts: [
|
|
@@ -217,6 +232,11 @@ const AGENT_REGISTRY = [
|
|
|
217
232
|
skillsPaths: { global: '.codex/skills', project: '.agents/skills' },
|
|
218
233
|
instructionFile: { global: '.codex/instructions.md', project: 'AGENTS.md' },
|
|
219
234
|
firstClass: true,
|
|
235
|
+
resumeCapability: {
|
|
236
|
+
mode: 'cli-resume',
|
|
237
|
+
nativeBundleFormat: 'codex-rollout',
|
|
238
|
+
cliResumeCommand: 'codex resume {uuid}',
|
|
239
|
+
},
|
|
220
240
|
quickStart: {
|
|
221
241
|
launchHint: 'Open your terminal and type `codex`',
|
|
222
242
|
skillCommand: 'Check AGENTS.md for workspace skills',
|
|
@@ -280,6 +300,44 @@ const AGENT_REGISTRY = [
|
|
|
280
300
|
],
|
|
281
301
|
},
|
|
282
302
|
},
|
|
303
|
+
{
|
|
304
|
+
// Google Antigravity - agent-first IDE built as a VS Code fork (Electron).
|
|
305
|
+
// Ships with Gemini 3 Pro; also supports Claude Sonnet/Opus. Skill and MCP
|
|
306
|
+
// config paths are under ~/.gemini/antigravity/ (shared Google AI namespace
|
|
307
|
+
// with Gemini CLI, but a distinct product line). The `antigravity` binary
|
|
308
|
+
// on PATH is the Electron launcher; on macOS we also detect via the
|
|
309
|
+
// installed app bundle so the registry recognizes installs that did not
|
|
310
|
+
// expose a CLI launcher.
|
|
311
|
+
slug: 'antigravity',
|
|
312
|
+
name: 'Antigravity',
|
|
313
|
+
aliases: ['Antigravity (Google)'],
|
|
314
|
+
description: "Google's agent-first IDE for building with AI -- Editor view for code, Manager view to orchestrate multiple agents in parallel",
|
|
315
|
+
category: 'ide',
|
|
316
|
+
detection: {
|
|
317
|
+
method: 'any',
|
|
318
|
+
target: [
|
|
319
|
+
{ method: 'binary', target: 'antigravity' },
|
|
320
|
+
{ method: 'path', target: { macos: '/Applications/Antigravity.app' } },
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
launch: {
|
|
324
|
+
app: { macos: 'Antigravity', windows: 'Antigravity' },
|
|
325
|
+
cli: 'antigravity',
|
|
326
|
+
},
|
|
327
|
+
logo: 'antigravity',
|
|
328
|
+
downloadUrl: 'https://antigravity.google',
|
|
329
|
+
skillsPaths: { global: '.gemini/antigravity/skills', project: '.agent/skills' },
|
|
330
|
+
mcpConfigPath: '.gemini/antigravity/mcp_config.json',
|
|
331
|
+
mcpConfigKey: 'mcpServers',
|
|
332
|
+
firstClass: true,
|
|
333
|
+
quickStart: {
|
|
334
|
+
launchHint: 'Open Antigravity',
|
|
335
|
+
examplePrompts: [
|
|
336
|
+
'What workspace skills do I have available?',
|
|
337
|
+
'Use Runwork tools to check team data',
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
283
341
|
// === Additional agents with some custom handling ===
|
|
284
342
|
{
|
|
285
343
|
slug: 'copilot-vscode',
|
|
@@ -316,7 +374,6 @@ const AGENT_REGISTRY = [
|
|
|
316
374
|
mcpConfigKey: 'mcpServers',
|
|
317
375
|
},
|
|
318
376
|
// === Community agents (from skillshare targets.yaml) ===
|
|
319
|
-
{ slug: 'antigravity', name: 'Antigravity', aliases: ['Antigravity (Google)'], description: "Google's Antigravity AI agent", category: 'cli', detection: { method: 'binary', target: 'antigravity' }, logo: 'antigravity', launch: { cli: 'antigravity' }, skillsPaths: { global: '.gemini/antigravity/skills', project: '.agent/skills' } },
|
|
320
377
|
{ slug: 'amp', name: 'Amp', description: 'AI coding agent by Sourcegraph', category: 'cli', detection: { method: 'binary', target: 'amp' }, skillsPaths: { global: '.config/agents/skills', project: '.agents/skills' } },
|
|
321
378
|
{ slug: 'adal', name: 'AdaL', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'adal' }, skillsPaths: { global: '.adal/skills', project: '.adal/skills' } },
|
|
322
379
|
{ slug: 'astrbot', name: 'AstrBot', description: 'AI coding agent', category: 'cli', detection: { method: 'binary', target: 'astrbot' }, skillsPaths: { global: '.astrbot/data/skills', project: 'data/skills' } },
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime host-agent detection.
|
|
3
|
+
*
|
|
4
|
+
* Determines which AI agent the CLI was just invoked FROM (env vars,
|
|
5
|
+
* SessionStart hook info file, process tree). Used by `runwork share-convo`
|
|
6
|
+
* to locate the active conversation's native transcript file, and by
|
|
7
|
+
* `runwork resume` to bias the target agent toward "same as sender."
|
|
8
|
+
*
|
|
9
|
+
* This is distinct from install-time detection (./detection.ts), which
|
|
10
|
+
* answers "is agent X installed on this machine."
|
|
11
|
+
*
|
|
12
|
+
* Detection priority (highest to lowest fidelity):
|
|
13
|
+
*
|
|
14
|
+
* 1. SessionStart hook info file at ~/.runwork/sessions/<sessionId>.json,
|
|
15
|
+
* keyed by the agent-provided session ID from env. Contains the
|
|
16
|
+
* authoritative transcript_path plus rich metadata. Written by the
|
|
17
|
+
* Runwork plugin's SessionStart hook script.
|
|
18
|
+
*
|
|
19
|
+
* 2. Env-var signals (CLAUDE_CODE_SESSION_ID, CODEX_THREAD_ID) plus a
|
|
20
|
+
* filesystem walk to locate the JSONL/rollout for that session ID.
|
|
21
|
+
*
|
|
22
|
+
* 3. Heuristic env vars (CLAUDECODE=1, CODEX_CI=1) when we know we're
|
|
23
|
+
* inside the agent but have no session ID. Falls back to mtime-newest
|
|
24
|
+
* file in the agent's session directory.
|
|
25
|
+
*
|
|
26
|
+
* 4. None detected: caller may still proceed with `--source-agent` override
|
|
27
|
+
* from the LLM (LLM passed `--native-file` and `--source-agent`).
|
|
28
|
+
*/
|
|
29
|
+
/** Slug values mirror packages/runwork-cli/src/agents/registry-data.ts AGENT_REGISTRY. */
|
|
30
|
+
export type DetectedAgentSlug = 'claude-code' | 'claude-desktop' | 'codex' | 'codex-app' | 'gemini' | 'cursor' | 'windsurf' | 'cline';
|
|
31
|
+
export interface RuntimeAgentDetection {
|
|
32
|
+
slug: DetectedAgentSlug;
|
|
33
|
+
sessionId: string | null;
|
|
34
|
+
/** Absolute path to the agent's native transcript file for this session, if known. */
|
|
35
|
+
sessionFilePath: string | null;
|
|
36
|
+
/** Source of the detection, useful for `runwork doctor --verbose` */
|
|
37
|
+
source: 'hook-info-file' | 'env-var' | 'heuristic';
|
|
38
|
+
/** Hook-info-file contents when source='hook-info-file' (richer metadata) */
|
|
39
|
+
hookInfo?: SessionHookInfo;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Shape of `~/.runwork/sessions/<sessionId>.json` files written by the
|
|
43
|
+
* SessionStart hook script.
|
|
44
|
+
*/
|
|
45
|
+
export interface SessionHookInfo {
|
|
46
|
+
writtenAt: string;
|
|
47
|
+
agent: DetectedAgentSlug;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
transcriptPath?: string | null;
|
|
50
|
+
cwd?: string;
|
|
51
|
+
gitBranch?: string | null;
|
|
52
|
+
gitRemote?: string | null;
|
|
53
|
+
model?: string;
|
|
54
|
+
mcpServers?: string[];
|
|
55
|
+
plugins?: string[];
|
|
56
|
+
agentVersion?: string;
|
|
57
|
+
pid?: number;
|
|
58
|
+
originalPayload?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Primary entrypoint. Returns the best-effort identification of the active
|
|
62
|
+
* host agent. `null` only if no signals at all are present.
|
|
63
|
+
*/
|
|
64
|
+
export declare function detectCurrentAgent(): RuntimeAgentDetection | null;
|
|
65
|
+
/**
|
|
66
|
+
* Read the SessionStart hook's info file for a given session ID, if present.
|
|
67
|
+
* Returns null when the hook hasn't fired yet (first invocation in the session
|
|
68
|
+
* before the hook ran, or the user hasn't installed the hook).
|
|
69
|
+
*/
|
|
70
|
+
export declare function readHookSessionInfo(sessionId: string): SessionHookInfo | null;
|
|
71
|
+
/**
|
|
72
|
+
* Walk ~/.claude/projects/*\/<sessionId>.jsonl for the matching file.
|
|
73
|
+
* Robust against the cwd-encoding rule changing or the recipient running from
|
|
74
|
+
* a different cwd than the sender.
|
|
75
|
+
*/
|
|
76
|
+
export declare function findClaudeCodeSessionFile(sessionId: string): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Walk ~/.codex/sessions/YYYY/MM/DD/rollout-*-<UUID>.jsonl for the file
|
|
79
|
+
* matching this thread/session ID. The codex CLI itself does this scan,
|
|
80
|
+
* so dropping the file anywhere in the tree works for resume too.
|
|
81
|
+
*/
|
|
82
|
+
export declare function findCodexRolloutFile(threadId: string): string | null;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime host-agent detection.
|
|
3
|
+
*
|
|
4
|
+
* Determines which AI agent the CLI was just invoked FROM (env vars,
|
|
5
|
+
* SessionStart hook info file, process tree). Used by `runwork share-convo`
|
|
6
|
+
* to locate the active conversation's native transcript file, and by
|
|
7
|
+
* `runwork resume` to bias the target agent toward "same as sender."
|
|
8
|
+
*
|
|
9
|
+
* This is distinct from install-time detection (./detection.ts), which
|
|
10
|
+
* answers "is agent X installed on this machine."
|
|
11
|
+
*
|
|
12
|
+
* Detection priority (highest to lowest fidelity):
|
|
13
|
+
*
|
|
14
|
+
* 1. SessionStart hook info file at ~/.runwork/sessions/<sessionId>.json,
|
|
15
|
+
* keyed by the agent-provided session ID from env. Contains the
|
|
16
|
+
* authoritative transcript_path plus rich metadata. Written by the
|
|
17
|
+
* Runwork plugin's SessionStart hook script.
|
|
18
|
+
*
|
|
19
|
+
* 2. Env-var signals (CLAUDE_CODE_SESSION_ID, CODEX_THREAD_ID) plus a
|
|
20
|
+
* filesystem walk to locate the JSONL/rollout for that session ID.
|
|
21
|
+
*
|
|
22
|
+
* 3. Heuristic env vars (CLAUDECODE=1, CODEX_CI=1) when we know we're
|
|
23
|
+
* inside the agent but have no session ID. Falls back to mtime-newest
|
|
24
|
+
* file in the agent's session directory.
|
|
25
|
+
*
|
|
26
|
+
* 4. None detected: caller may still proceed with `--source-agent` override
|
|
27
|
+
* from the LLM (LLM passed `--native-file` and `--source-agent`).
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
|
30
|
+
import { homedir } from 'os';
|
|
31
|
+
import { join } from 'path';
|
|
32
|
+
const RUNWORK_SESSIONS_DIR = join(homedir(), '.runwork', 'sessions');
|
|
33
|
+
/**
|
|
34
|
+
* Primary entrypoint. Returns the best-effort identification of the active
|
|
35
|
+
* host agent. `null` only if no signals at all are present.
|
|
36
|
+
*/
|
|
37
|
+
export function detectCurrentAgent() {
|
|
38
|
+
// 1. Env-var-based session IDs (highest signal): use them to look up the
|
|
39
|
+
// hook info file first, then fall back to env+filesystem walk.
|
|
40
|
+
const claudeCodeSessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
41
|
+
if (claudeCodeSessionId) {
|
|
42
|
+
const hookInfo = readHookSessionInfo(claudeCodeSessionId);
|
|
43
|
+
if (hookInfo) {
|
|
44
|
+
return {
|
|
45
|
+
slug: hookInfo.agent,
|
|
46
|
+
sessionId: claudeCodeSessionId,
|
|
47
|
+
sessionFilePath: hookInfo.transcriptPath ?? null,
|
|
48
|
+
source: 'hook-info-file',
|
|
49
|
+
hookInfo,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const filePath = findClaudeCodeSessionFile(claudeCodeSessionId);
|
|
53
|
+
return {
|
|
54
|
+
slug: 'claude-code',
|
|
55
|
+
sessionId: claudeCodeSessionId,
|
|
56
|
+
sessionFilePath: filePath,
|
|
57
|
+
source: 'env-var',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const codexThreadId = process.env.CODEX_THREAD_ID;
|
|
61
|
+
if (codexThreadId) {
|
|
62
|
+
const hookInfo = readHookSessionInfo(codexThreadId);
|
|
63
|
+
if (hookInfo) {
|
|
64
|
+
return {
|
|
65
|
+
slug: hookInfo.agent,
|
|
66
|
+
sessionId: codexThreadId,
|
|
67
|
+
sessionFilePath: hookInfo.transcriptPath ?? null,
|
|
68
|
+
source: 'hook-info-file',
|
|
69
|
+
hookInfo,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const filePath = findCodexRolloutFile(codexThreadId);
|
|
73
|
+
return {
|
|
74
|
+
slug: 'codex',
|
|
75
|
+
sessionId: codexThreadId,
|
|
76
|
+
sessionFilePath: filePath,
|
|
77
|
+
source: 'env-var',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// 2. Heuristic env vars - we know we're inside the agent but session ID
|
|
81
|
+
// was not exposed. mtime-newest fallback.
|
|
82
|
+
if (process.env.CLAUDECODE === '1' || process.env.CLAUDECODE === 'true') {
|
|
83
|
+
const newest = findNewestClaudeCodeSession();
|
|
84
|
+
return {
|
|
85
|
+
slug: 'claude-code',
|
|
86
|
+
sessionId: newest?.sessionId ?? null,
|
|
87
|
+
sessionFilePath: newest?.path ?? null,
|
|
88
|
+
source: 'heuristic',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (process.env.CODEX_CI === '1') {
|
|
92
|
+
const newest = findNewestCodexRollout();
|
|
93
|
+
return {
|
|
94
|
+
slug: 'codex',
|
|
95
|
+
sessionId: newest?.sessionId ?? null,
|
|
96
|
+
sessionFilePath: newest?.path ?? null,
|
|
97
|
+
source: 'heuristic',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read the SessionStart hook's info file for a given session ID, if present.
|
|
104
|
+
* Returns null when the hook hasn't fired yet (first invocation in the session
|
|
105
|
+
* before the hook ran, or the user hasn't installed the hook).
|
|
106
|
+
*/
|
|
107
|
+
export function readHookSessionInfo(sessionId) {
|
|
108
|
+
const path = join(RUNWORK_SESSIONS_DIR, `${sessionId}.json`);
|
|
109
|
+
if (!existsSync(path))
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const raw = readFileSync(path, 'utf8');
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Walk ~/.claude/projects/*\/<sessionId>.jsonl for the matching file.
|
|
122
|
+
* Robust against the cwd-encoding rule changing or the recipient running from
|
|
123
|
+
* a different cwd than the sender.
|
|
124
|
+
*/
|
|
125
|
+
export function findClaudeCodeSessionFile(sessionId) {
|
|
126
|
+
const root = join(homedir(), '.claude', 'projects');
|
|
127
|
+
if (!existsSync(root))
|
|
128
|
+
return null;
|
|
129
|
+
let projectDirs;
|
|
130
|
+
try {
|
|
131
|
+
projectDirs = readdirSync(root);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
for (const dir of projectDirs) {
|
|
137
|
+
const candidate = join(root, dir, `${sessionId}.jsonl`);
|
|
138
|
+
if (existsSync(candidate))
|
|
139
|
+
return candidate;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Walk ~/.codex/sessions/YYYY/MM/DD/rollout-*-<UUID>.jsonl for the file
|
|
145
|
+
* matching this thread/session ID. The codex CLI itself does this scan,
|
|
146
|
+
* so dropping the file anywhere in the tree works for resume too.
|
|
147
|
+
*/
|
|
148
|
+
export function findCodexRolloutFile(threadId) {
|
|
149
|
+
const root = join(homedir(), '.codex', 'sessions');
|
|
150
|
+
if (!existsSync(root))
|
|
151
|
+
return null;
|
|
152
|
+
const stack = [root];
|
|
153
|
+
while (stack.length > 0) {
|
|
154
|
+
const dir = stack.pop();
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = readdirSync(dir);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const full = join(dir, entry);
|
|
164
|
+
let s;
|
|
165
|
+
try {
|
|
166
|
+
s = statSync(full);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (s.isDirectory()) {
|
|
172
|
+
stack.push(full);
|
|
173
|
+
}
|
|
174
|
+
else if (s.isFile() && entry.includes(threadId) && entry.endsWith('.jsonl')) {
|
|
175
|
+
return full;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Fallback when no session ID env var is set: scan all project folders for
|
|
183
|
+
* the most recently modified .jsonl. Fragile with multiple concurrent
|
|
184
|
+
* Claude Code windows; documented as a known limitation.
|
|
185
|
+
*/
|
|
186
|
+
function findNewestClaudeCodeSession() {
|
|
187
|
+
const root = join(homedir(), '.claude', 'projects');
|
|
188
|
+
if (!existsSync(root))
|
|
189
|
+
return null;
|
|
190
|
+
let projectDirs;
|
|
191
|
+
try {
|
|
192
|
+
projectDirs = readdirSync(root);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
let best = null;
|
|
198
|
+
for (const dir of projectDirs) {
|
|
199
|
+
const projectPath = join(root, dir);
|
|
200
|
+
let files;
|
|
201
|
+
try {
|
|
202
|
+
files = readdirSync(projectPath);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
if (!file.endsWith('.jsonl'))
|
|
209
|
+
continue;
|
|
210
|
+
const full = join(projectPath, file);
|
|
211
|
+
try {
|
|
212
|
+
const s = statSync(full);
|
|
213
|
+
if (!best || s.mtimeMs > best.mtime) {
|
|
214
|
+
best = {
|
|
215
|
+
sessionId: file.replace(/\.jsonl$/, ''),
|
|
216
|
+
path: full,
|
|
217
|
+
mtime: s.mtimeMs,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return best ? { sessionId: best.sessionId, path: best.path } : null;
|
|
227
|
+
}
|
|
228
|
+
function findNewestCodexRollout() {
|
|
229
|
+
const root = join(homedir(), '.codex', 'sessions');
|
|
230
|
+
if (!existsSync(root))
|
|
231
|
+
return null;
|
|
232
|
+
const stack = [root];
|
|
233
|
+
let best = null;
|
|
234
|
+
while (stack.length > 0) {
|
|
235
|
+
const dir = stack.pop();
|
|
236
|
+
let entries;
|
|
237
|
+
try {
|
|
238
|
+
entries = readdirSync(dir);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const full = join(dir, entry);
|
|
245
|
+
let s;
|
|
246
|
+
try {
|
|
247
|
+
s = statSync(full);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (s.isDirectory()) {
|
|
253
|
+
stack.push(full);
|
|
254
|
+
}
|
|
255
|
+
else if (s.isFile() && entry.startsWith('rollout-') && entry.endsWith('.jsonl')) {
|
|
256
|
+
// Extract UUID from filename: rollout-YYYY-MM-DDThh-mm-ss-<uuid>.jsonl
|
|
257
|
+
const match = entry.match(/rollout-[\d-T:]+-([0-9a-f-]{36})\.jsonl$/);
|
|
258
|
+
if (!match)
|
|
259
|
+
continue;
|
|
260
|
+
if (!best || s.mtimeMs > best.mtime) {
|
|
261
|
+
best = {
|
|
262
|
+
sessionId: match[1],
|
|
263
|
+
path: full,
|
|
264
|
+
mtime: s.mtimeMs,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return best ? { sessionId: best.sessionId, path: best.path } : null;
|
|
271
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStart hook script for Claude Code (and any other agent that
|
|
3
|
+
* follows the same hook contract: stdin JSON with session_id and
|
|
4
|
+
* transcript_path).
|
|
5
|
+
*
|
|
6
|
+
* SAFETY CONTRACT - this hook MUST NEVER break the user's session.
|
|
7
|
+
*
|
|
8
|
+
* Failure modes a hook can cause in Claude Code:
|
|
9
|
+
* - Non-zero exit -> Claude may surface an error or refuse to start
|
|
10
|
+
* - Stdout contamination -> Claude may interpret it as a hook response
|
|
11
|
+
* - Hanging -> Blocks the session indefinitely
|
|
12
|
+
* - Stderr noise -> Pollutes the user's terminal
|
|
13
|
+
*
|
|
14
|
+
* Mitigations applied:
|
|
15
|
+
* - No `set -e`. Every operation is fault-tolerant with || true.
|
|
16
|
+
* - All stdout/stderr from internal operations are redirected to /dev/null.
|
|
17
|
+
* - A hard wall-clock timeout via a backgrounded watchdog that SIGKILLs the
|
|
18
|
+
* hook if it runs longer than 5 seconds.
|
|
19
|
+
* - Wrapped in a "{...} </dev/null/stdin; echo nothing" pattern so failures
|
|
20
|
+
* never propagate to Claude.
|
|
21
|
+
* - Always exits 0, regardless of any internal failure.
|
|
22
|
+
*
|
|
23
|
+
* The hook does best-effort logging to a side-channel file
|
|
24
|
+
* (~/.runwork/sessions-hook-debug.log, last 50 entries) so we can debug
|
|
25
|
+
* issues without ever talking to Claude's stdio.
|
|
26
|
+
*
|
|
27
|
+
* Installed into the user's Claude Code plugin tree by sync. When Claude
|
|
28
|
+
* Code starts a new session, this script runs once and writes
|
|
29
|
+
* `~/.runwork/sessions/<sessionId>.json` with rich session metadata. The
|
|
30
|
+
* runwork CLI's runtime-detection module reads from that location.
|
|
31
|
+
*
|
|
32
|
+
* Hook installation locations:
|
|
33
|
+
* - Claude Code (sync): ~/.claude/plugins/cache/runwork/<plugin>/<v>/hooks/
|
|
34
|
+
* + marketplace mirror
|
|
35
|
+
* - Claude Desktop (Cowork): inside the runwork-plugin.zip the user uploads
|
|
36
|
+
*/
|
|
37
|
+
export declare const SESSION_START_HOOK_SCRIPT = "#!/usr/bin/env bash\n# Runwork SessionStart hook - SAFETY-FIRST design.\n#\n# Writes ~/.runwork/sessions/<sessionId>.json with rich session metadata.\n# Generated by the runwork CLI on sync; do not edit by hand.\n#\n# Hard guarantees:\n# - Exits 0 no matter what.\n# - Never writes to Claude's stdout (would corrupt the hook response).\n# - Never hangs (5-second wall-clock kill via watchdog).\n# - Never errors out into Claude's stderr.\n\n# Read full payload from stdin BEFORE we redirect anything; this is the one\n# thing we can't lose.\nPAYLOAD=\"\"\nif [ ! -t 0 ]; then\n PAYLOAD=$(cat 2>/dev/null || true)\nfi\n\n# Suppress every potential side-channel: redirect stdout/stderr to /dev/null\n# for the rest of execution. Claude reads the hook's stdout as a structured\n# response, so any accidental print would be interpreted as protocol data.\nexec 1>/dev/null 2>/dev/null\n\n# Watchdog: kill this hook after 5 seconds no matter what.\n( sleep 5 && kill -9 $$ 2>/dev/null ) &\nWATCHDOG_PID=$!\n# Ensure the watchdog is cleaned up if we exit normally.\ntrap \"kill -9 $WATCHDOG_PID 2>/dev/null; exit 0\" EXIT INT TERM\n\n# Everything below runs in a \"best-effort, never-fail\" mode. We capture\n# extras via subshells with /dev/null redirection and || true so nothing\n# can escape.\n{\n RUNWORK_DIR=\"${HOME:-/tmp}/.runwork\"\n SESSIONS_DIR=\"$RUNWORK_DIR/sessions\"\n DEBUG_LOG=\"$RUNWORK_DIR/sessions-hook-debug.log\"\n\n mkdir -p \"$SESSIONS_DIR\" 2>/dev/null || exit 0\n\n # Bail silently if no payload.\n if [ -z \"$PAYLOAD\" ]; then\n exit 0\n fi\n\n # Parse session_id and transcript_path. Prefer jq when available.\n SESSION_ID=\"\"\n TRANSCRIPT_PATH=\"\"\n CWD=\"\"\n\n if command -v jq >/dev/null 2>&1; then\n SESSION_ID=$(echo \"$PAYLOAD\" | jq -r '.session_id // .sessionId // empty' 2>/dev/null || true)\n TRANSCRIPT_PATH=$(echo \"$PAYLOAD\" | jq -r '.transcript_path // .transcriptPath // empty' 2>/dev/null || true)\n CWD=$(echo \"$PAYLOAD\" | jq -r '.cwd // empty' 2>/dev/null || true)\n else\n # Minimal grep+sed fallback - intentionally permissive about formatting.\n SESSION_ID=$(echo \"$PAYLOAD\" | grep -oE '\"session_id\"[[:space:]]*:[[:space:]]*\"[^\"]+\"' 2>/dev/null | head -1 | sed -E 's/.*\"([^\"]+)\"$/\\1/' || true)\n TRANSCRIPT_PATH=$(echo \"$PAYLOAD\" | grep -oE '\"transcript_path\"[[:space:]]*:[[:space:]]*\"[^\"]+\"' 2>/dev/null | head -1 | sed -E 's/.*\"([^\"]+)\"$/\\1/' || true)\n CWD=$(echo \"$PAYLOAD\" | grep -oE '\"cwd\"[[:space:]]*:[[:space:]]*\"[^\"]+\"' 2>/dev/null | head -1 | sed -E 's/.*\"([^\"]+)\"$/\\1/' || true)\n fi\n\n # Without a session ID we have nothing useful to write.\n if [ -z \"$SESSION_ID\" ]; then\n exit 0\n fi\n\n # Sanity check sessionId looks like a safe filename component\n # (alphanumeric, hyphens, underscores only). Anything weird -> bail.\n case \"$SESSION_ID\" in\n *[!a-zA-Z0-9_-]*) exit 0 ;;\n esac\n\n EFFECTIVE_CWD=\"${CWD:-${PWD:-/}}\"\n\n GIT_BRANCH=\"\"\n GIT_REMOTE=\"\"\n if [ -n \"$EFFECTIVE_CWD\" ] && [ -d \"$EFFECTIVE_CWD\" ]; then\n GIT_BRANCH=$(git -C \"$EFFECTIVE_CWD\" rev-parse --abbrev-ref HEAD 2>/dev/null || true)\n GIT_REMOTE=$(git -C \"$EFFECTIVE_CWD\" config --get remote.origin.url 2>/dev/null | sed -E 's#.*[:/]([^/:]+/[^/]+)(\\.git)?$#\\1#' 2>/dev/null || true)\n fi\n\n AGENT_VERSION=$(claude --version 2>/dev/null | head -1 || true)\n\n OUT_PATH=\"$SESSIONS_DIR/${SESSION_ID}.json\"\n TMP_PATH=\"${OUT_PATH}.tmp.$$\"\n\n # Build JSON safely; values that may contain quotes/backslashes are escaped\n # via a tiny helper. We avoid heredoc + interpolation pitfalls.\n json_escape() {\n # Escape backslash and double-quote, strip newlines.\n printf '%s' \"$1\" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/\"/\\\\\"/g' | tr -d '\\n'\n }\n\n TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)\n\n {\n printf '{\\n'\n printf ' \"writtenAt\": \"%s\",\\n' \"$(json_escape \"$TIMESTAMP\")\"\n printf ' \"agent\": \"claude-code\",\\n'\n printf ' \"sessionId\": \"%s\",\\n' \"$(json_escape \"$SESSION_ID\")\"\n printf ' \"transcriptPath\": \"%s\",\\n' \"$(json_escape \"$TRANSCRIPT_PATH\")\"\n printf ' \"cwd\": \"%s\",\\n' \"$(json_escape \"$EFFECTIVE_CWD\")\"\n printf ' \"gitBranch\": \"%s\",\\n' \"$(json_escape \"$GIT_BRANCH\")\"\n printf ' \"gitRemote\": \"%s\",\\n' \"$(json_escape \"$GIT_REMOTE\")\"\n printf ' \"agentVersion\": \"%s\",\\n' \"$(json_escape \"$AGENT_VERSION\")\"\n printf ' \"pid\": %d\\n' \"$$\"\n printf '}\\n'\n } > \"$TMP_PATH\" 2>/dev/null || exit 0\n\n mv \"$TMP_PATH\" \"$OUT_PATH\" 2>/dev/null || true\n\n # Best-effort debug log (rotated to last 50 lines).\n {\n echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) wrote $OUT_PATH\"\n if [ -f \"$DEBUG_LOG\" ]; then\n tail -49 \"$DEBUG_LOG\" 2>/dev/null || true\n fi\n } > \"${DEBUG_LOG}.tmp\" 2>/dev/null && mv \"${DEBUG_LOG}.tmp\" \"$DEBUG_LOG\" 2>/dev/null || true\n} 2>/dev/null || true\n\n# Hard guarantee.\nexit 0\n";
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStart hook script for Claude Code (and any other agent that
|
|
3
|
+
* follows the same hook contract: stdin JSON with session_id and
|
|
4
|
+
* transcript_path).
|
|
5
|
+
*
|
|
6
|
+
* SAFETY CONTRACT - this hook MUST NEVER break the user's session.
|
|
7
|
+
*
|
|
8
|
+
* Failure modes a hook can cause in Claude Code:
|
|
9
|
+
* - Non-zero exit -> Claude may surface an error or refuse to start
|
|
10
|
+
* - Stdout contamination -> Claude may interpret it as a hook response
|
|
11
|
+
* - Hanging -> Blocks the session indefinitely
|
|
12
|
+
* - Stderr noise -> Pollutes the user's terminal
|
|
13
|
+
*
|
|
14
|
+
* Mitigations applied:
|
|
15
|
+
* - No `set -e`. Every operation is fault-tolerant with || true.
|
|
16
|
+
* - All stdout/stderr from internal operations are redirected to /dev/null.
|
|
17
|
+
* - A hard wall-clock timeout via a backgrounded watchdog that SIGKILLs the
|
|
18
|
+
* hook if it runs longer than 5 seconds.
|
|
19
|
+
* - Wrapped in a "{...} </dev/null/stdin; echo nothing" pattern so failures
|
|
20
|
+
* never propagate to Claude.
|
|
21
|
+
* - Always exits 0, regardless of any internal failure.
|
|
22
|
+
*
|
|
23
|
+
* The hook does best-effort logging to a side-channel file
|
|
24
|
+
* (~/.runwork/sessions-hook-debug.log, last 50 entries) so we can debug
|
|
25
|
+
* issues without ever talking to Claude's stdio.
|
|
26
|
+
*
|
|
27
|
+
* Installed into the user's Claude Code plugin tree by sync. When Claude
|
|
28
|
+
* Code starts a new session, this script runs once and writes
|
|
29
|
+
* `~/.runwork/sessions/<sessionId>.json` with rich session metadata. The
|
|
30
|
+
* runwork CLI's runtime-detection module reads from that location.
|
|
31
|
+
*
|
|
32
|
+
* Hook installation locations:
|
|
33
|
+
* - Claude Code (sync): ~/.claude/plugins/cache/runwork/<plugin>/<v>/hooks/
|
|
34
|
+
* + marketplace mirror
|
|
35
|
+
* - Claude Desktop (Cowork): inside the runwork-plugin.zip the user uploads
|
|
36
|
+
*/
|
|
37
|
+
export const SESSION_START_HOOK_SCRIPT = `#!/usr/bin/env bash
|
|
38
|
+
# Runwork SessionStart hook - SAFETY-FIRST design.
|
|
39
|
+
#
|
|
40
|
+
# Writes ~/.runwork/sessions/<sessionId>.json with rich session metadata.
|
|
41
|
+
# Generated by the runwork CLI on sync; do not edit by hand.
|
|
42
|
+
#
|
|
43
|
+
# Hard guarantees:
|
|
44
|
+
# - Exits 0 no matter what.
|
|
45
|
+
# - Never writes to Claude's stdout (would corrupt the hook response).
|
|
46
|
+
# - Never hangs (5-second wall-clock kill via watchdog).
|
|
47
|
+
# - Never errors out into Claude's stderr.
|
|
48
|
+
|
|
49
|
+
# Read full payload from stdin BEFORE we redirect anything; this is the one
|
|
50
|
+
# thing we can't lose.
|
|
51
|
+
PAYLOAD=""
|
|
52
|
+
if [ ! -t 0 ]; then
|
|
53
|
+
PAYLOAD=$(cat 2>/dev/null || true)
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Suppress every potential side-channel: redirect stdout/stderr to /dev/null
|
|
57
|
+
# for the rest of execution. Claude reads the hook's stdout as a structured
|
|
58
|
+
# response, so any accidental print would be interpreted as protocol data.
|
|
59
|
+
exec 1>/dev/null 2>/dev/null
|
|
60
|
+
|
|
61
|
+
# Watchdog: kill this hook after 5 seconds no matter what.
|
|
62
|
+
( sleep 5 && kill -9 $$ 2>/dev/null ) &
|
|
63
|
+
WATCHDOG_PID=$!
|
|
64
|
+
# Ensure the watchdog is cleaned up if we exit normally.
|
|
65
|
+
trap "kill -9 $WATCHDOG_PID 2>/dev/null; exit 0" EXIT INT TERM
|
|
66
|
+
|
|
67
|
+
# Everything below runs in a "best-effort, never-fail" mode. We capture
|
|
68
|
+
# extras via subshells with /dev/null redirection and || true so nothing
|
|
69
|
+
# can escape.
|
|
70
|
+
{
|
|
71
|
+
RUNWORK_DIR="\${HOME:-/tmp}/.runwork"
|
|
72
|
+
SESSIONS_DIR="$RUNWORK_DIR/sessions"
|
|
73
|
+
DEBUG_LOG="$RUNWORK_DIR/sessions-hook-debug.log"
|
|
74
|
+
|
|
75
|
+
mkdir -p "$SESSIONS_DIR" 2>/dev/null || exit 0
|
|
76
|
+
|
|
77
|
+
# Bail silently if no payload.
|
|
78
|
+
if [ -z "$PAYLOAD" ]; then
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Parse session_id and transcript_path. Prefer jq when available.
|
|
83
|
+
SESSION_ID=""
|
|
84
|
+
TRANSCRIPT_PATH=""
|
|
85
|
+
CWD=""
|
|
86
|
+
|
|
87
|
+
if command -v jq >/dev/null 2>&1; then
|
|
88
|
+
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // .sessionId // empty' 2>/dev/null || true)
|
|
89
|
+
TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // .transcriptPath // empty' 2>/dev/null || true)
|
|
90
|
+
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
91
|
+
else
|
|
92
|
+
# Minimal grep+sed fallback - intentionally permissive about formatting.
|
|
93
|
+
SESSION_ID=$(echo "$PAYLOAD" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]+"' 2>/dev/null | head -1 | sed -E 's/.*"([^"]+)"$/\\1/' || true)
|
|
94
|
+
TRANSCRIPT_PATH=$(echo "$PAYLOAD" | grep -oE '"transcript_path"[[:space:]]*:[[:space:]]*"[^"]+"' 2>/dev/null | head -1 | sed -E 's/.*"([^"]+)"$/\\1/' || true)
|
|
95
|
+
CWD=$(echo "$PAYLOAD" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]+"' 2>/dev/null | head -1 | sed -E 's/.*"([^"]+)"$/\\1/' || true)
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Without a session ID we have nothing useful to write.
|
|
99
|
+
if [ -z "$SESSION_ID" ]; then
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Sanity check sessionId looks like a safe filename component
|
|
104
|
+
# (alphanumeric, hyphens, underscores only). Anything weird -> bail.
|
|
105
|
+
case "$SESSION_ID" in
|
|
106
|
+
*[!a-zA-Z0-9_-]*) exit 0 ;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
EFFECTIVE_CWD="\${CWD:-\${PWD:-/}}"
|
|
110
|
+
|
|
111
|
+
GIT_BRANCH=""
|
|
112
|
+
GIT_REMOTE=""
|
|
113
|
+
if [ -n "$EFFECTIVE_CWD" ] && [ -d "$EFFECTIVE_CWD" ]; then
|
|
114
|
+
GIT_BRANCH=$(git -C "$EFFECTIVE_CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
|
115
|
+
GIT_REMOTE=$(git -C "$EFFECTIVE_CWD" config --get remote.origin.url 2>/dev/null | sed -E 's#.*[:/]([^/:]+/[^/]+)(\\.git)?$#\\1#' 2>/dev/null || true)
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
AGENT_VERSION=$(claude --version 2>/dev/null | head -1 || true)
|
|
119
|
+
|
|
120
|
+
OUT_PATH="$SESSIONS_DIR/\${SESSION_ID}.json"
|
|
121
|
+
TMP_PATH="\${OUT_PATH}.tmp.$$"
|
|
122
|
+
|
|
123
|
+
# Build JSON safely; values that may contain quotes/backslashes are escaped
|
|
124
|
+
# via a tiny helper. We avoid heredoc + interpolation pitfalls.
|
|
125
|
+
json_escape() {
|
|
126
|
+
# Escape backslash and double-quote, strip newlines.
|
|
127
|
+
printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' | tr -d '\\n'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
printf '{\\n'
|
|
134
|
+
printf ' "writtenAt": "%s",\\n' "$(json_escape "$TIMESTAMP")"
|
|
135
|
+
printf ' "agent": "claude-code",\\n'
|
|
136
|
+
printf ' "sessionId": "%s",\\n' "$(json_escape "$SESSION_ID")"
|
|
137
|
+
printf ' "transcriptPath": "%s",\\n' "$(json_escape "$TRANSCRIPT_PATH")"
|
|
138
|
+
printf ' "cwd": "%s",\\n' "$(json_escape "$EFFECTIVE_CWD")"
|
|
139
|
+
printf ' "gitBranch": "%s",\\n' "$(json_escape "$GIT_BRANCH")"
|
|
140
|
+
printf ' "gitRemote": "%s",\\n' "$(json_escape "$GIT_REMOTE")"
|
|
141
|
+
printf ' "agentVersion": "%s",\\n' "$(json_escape "$AGENT_VERSION")"
|
|
142
|
+
printf ' "pid": %d\\n' "$$"
|
|
143
|
+
printf '}\\n'
|
|
144
|
+
} > "$TMP_PATH" 2>/dev/null || exit 0
|
|
145
|
+
|
|
146
|
+
mv "$TMP_PATH" "$OUT_PATH" 2>/dev/null || true
|
|
147
|
+
|
|
148
|
+
# Best-effort debug log (rotated to last 50 lines).
|
|
149
|
+
{
|
|
150
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) wrote $OUT_PATH"
|
|
151
|
+
if [ -f "$DEBUG_LOG" ]; then
|
|
152
|
+
tail -49 "$DEBUG_LOG" 2>/dev/null || true
|
|
153
|
+
fi
|
|
154
|
+
} > "\${DEBUG_LOG}.tmp" 2>/dev/null && mv "\${DEBUG_LOG}.tmp" "$DEBUG_LOG" 2>/dev/null || true
|
|
155
|
+
} 2>/dev/null || true
|
|
156
|
+
|
|
157
|
+
# Hard guarantee.
|
|
158
|
+
exit 0
|
|
159
|
+
`;
|