devharness 0.9.8 → 0.9.9

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.
Files changed (117) hide show
  1. package/README.md +15 -0
  2. package/build/block-events.d.ts +3 -5
  3. package/build/block-events.d.ts.map +1 -1
  4. package/build/block-events.js +6 -24
  5. package/build/block-events.js.map +1 -1
  6. package/build/block-events.test.js +10 -7
  7. package/build/block-events.test.js.map +1 -1
  8. package/build/cdp-manager.d.ts +7 -0
  9. package/build/cdp-manager.d.ts.map +1 -1
  10. package/build/cdp-manager.js +9 -0
  11. package/build/cdp-manager.js.map +1 -1
  12. package/build/cli/index.d.ts +26 -0
  13. package/build/cli/index.d.ts.map +1 -0
  14. package/build/cli/index.js +290 -0
  15. package/build/cli/index.js.map +1 -0
  16. package/build/cli/process-tree.d.ts +21 -0
  17. package/build/cli/process-tree.d.ts.map +1 -0
  18. package/build/cli/process-tree.js +51 -0
  19. package/build/cli/process-tree.js.map +1 -0
  20. package/build/cli/session-match.d.ts +37 -0
  21. package/build/cli/session-match.d.ts.map +1 -0
  22. package/build/cli/session-match.js +55 -0
  23. package/build/cli/session-match.js.map +1 -0
  24. package/build/cli/session-match.test.d.ts +17 -0
  25. package/build/cli/session-match.test.d.ts.map +1 -0
  26. package/build/cli/session-match.test.js +125 -0
  27. package/build/cli/session-match.test.js.map +1 -0
  28. package/build/config.d.ts +1 -1
  29. package/build/config.d.ts.map +1 -1
  30. package/build/config.js +1 -0
  31. package/build/config.js.map +1 -1
  32. package/build/connection-manager.d.ts.map +1 -1
  33. package/build/connection-manager.js +6 -1
  34. package/build/connection-manager.js.map +1 -1
  35. package/build/index.js +57 -4
  36. package/build/index.js.map +1 -1
  37. package/build/mcp-supervisor.js +43 -0
  38. package/build/mcp-supervisor.js.map +1 -1
  39. package/build/network-monitor.d.ts +13 -2
  40. package/build/network-monitor.d.ts.map +1 -1
  41. package/build/network-monitor.js +38 -2
  42. package/build/network-monitor.js.map +1 -1
  43. package/build/network-monitor.test.js +50 -0
  44. package/build/network-monitor.test.js.map +1 -1
  45. package/build/plugin-session-start-hook.test.d.ts +10 -0
  46. package/build/plugin-session-start-hook.test.d.ts.map +1 -0
  47. package/build/plugin-session-start-hook.test.js +187 -0
  48. package/build/plugin-session-start-hook.test.js.map +1 -0
  49. package/build/session-endpoint.d.ts +72 -0
  50. package/build/session-endpoint.d.ts.map +1 -0
  51. package/build/session-endpoint.js +220 -0
  52. package/build/session-endpoint.js.map +1 -0
  53. package/build/session-endpoint.test.d.ts +10 -0
  54. package/build/session-endpoint.test.d.ts.map +1 -0
  55. package/build/session-endpoint.test.js +202 -0
  56. package/build/session-endpoint.test.js.map +1 -0
  57. package/build/session-events.d.ts +28 -0
  58. package/build/session-events.d.ts.map +1 -0
  59. package/build/session-events.js +38 -0
  60. package/build/session-events.js.map +1 -0
  61. package/build/session-events.test.d.ts +8 -0
  62. package/build/session-events.test.d.ts.map +1 -0
  63. package/build/session-events.test.js +56 -0
  64. package/build/session-events.test.js.map +1 -0
  65. package/build/session-identity.d.ts +56 -0
  66. package/build/session-identity.d.ts.map +1 -0
  67. package/build/session-identity.js +96 -0
  68. package/build/session-identity.js.map +1 -0
  69. package/build/session-identity.test.d.ts +9 -0
  70. package/build/session-identity.test.d.ts.map +1 -0
  71. package/build/session-identity.test.js +126 -0
  72. package/build/session-identity.test.js.map +1 -0
  73. package/build/session-messages.d.ts +70 -0
  74. package/build/session-messages.d.ts.map +1 -0
  75. package/build/session-messages.js +139 -0
  76. package/build/session-messages.js.map +1 -0
  77. package/build/tool-response.d.ts +40 -1
  78. package/build/tool-response.d.ts.map +1 -1
  79. package/build/tool-response.js.map +1 -1
  80. package/build/tools/console-tools.d.ts +4 -0
  81. package/build/tools/console-tools.d.ts.map +1 -1
  82. package/build/tools/console-tools.js +55 -1
  83. package/build/tools/console-tools.js.map +1 -1
  84. package/build/tools/inspect-worker-meta.test.d.ts +9 -0
  85. package/build/tools/inspect-worker-meta.test.d.ts.map +1 -0
  86. package/build/tools/inspect-worker-meta.test.js +122 -0
  87. package/build/tools/inspect-worker-meta.test.js.map +1 -0
  88. package/build/tools/inspection-tools.d.ts +12 -5
  89. package/build/tools/inspection-tools.d.ts.map +1 -1
  90. package/build/tools/inspection-tools.js +86 -5
  91. package/build/tools/inspection-tools.js.map +1 -1
  92. package/build/tools/issues-tools.d.ts +3 -3
  93. package/build/tools/message-tools.d.ts +51 -0
  94. package/build/tools/message-tools.d.ts.map +1 -0
  95. package/build/tools/message-tools.js +250 -0
  96. package/build/tools/message-tools.js.map +1 -0
  97. package/build/tools/message-tools.test.d.ts +9 -0
  98. package/build/tools/message-tools.test.d.ts.map +1 -0
  99. package/build/tools/message-tools.test.js +332 -0
  100. package/build/tools/message-tools.test.js.map +1 -0
  101. package/build/tools/network-tools.js +1 -1
  102. package/build/tools/network-tools.js.map +1 -1
  103. package/build/tools/replay-tools.d.ts +3 -3
  104. package/build/tools/server-tools.d.ts +3 -3
  105. package/build/worker-targets.d.ts +75 -0
  106. package/build/worker-targets.d.ts.map +1 -0
  107. package/build/worker-targets.js +171 -0
  108. package/build/worker-targets.js.map +1 -0
  109. package/build/worker-targets.test.d.ts +8 -0
  110. package/build/worker-targets.test.d.ts.map +1 -0
  111. package/build/worker-targets.test.js +118 -0
  112. package/build/worker-targets.test.js.map +1 -0
  113. package/docs/instructions.md +23 -12
  114. package/docs/messages.md +150 -0
  115. package/package.json +2 -1
  116. package/plugin/skills/devharness/SKILL.md +36 -8
  117. package/plugin/skills/devharness/references/tool-categories.md +8 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Tests for the socket a CLI process calls tools through.
3
+ *
4
+ * The round-trip cases bind a real socket in a temp DEVHARNESS_DIR and connect
5
+ * to it, because the things worth asserting - that a ToolError comes back as a
6
+ * renderable response, that a half-written request does not reply early - are
7
+ * properties of the framing, not of a mocked handler.
8
+ */
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import { connect } from 'net';
11
+ import { mkdtempSync, rmSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
12
+ import { tmpdir } from 'os';
13
+ import { join } from 'path';
14
+ import { initializePaths } from './helpers/paths.js';
15
+ import { startSessionEndpoint, listSessionRecords, getEndpointsDir, getEndpointAddress, getPresencePath, } from './session-endpoint.js';
16
+ const posixOnly = process.platform === 'win32' ? it.skip : it;
17
+ let dir;
18
+ let previousDir;
19
+ let endpoint = null;
20
+ beforeEach(() => {
21
+ previousDir = process.env.DEVHARNESS_DIR;
22
+ dir = mkdtempSync(join(tmpdir(), 'devharness-endpoint-'));
23
+ process.env.DEVHARNESS_DIR = dir;
24
+ initializePaths();
25
+ });
26
+ afterEach(async () => {
27
+ if (endpoint) {
28
+ await endpoint.close();
29
+ endpoint = null;
30
+ }
31
+ if (previousDir === undefined)
32
+ delete process.env.DEVHARNESS_DIR;
33
+ else
34
+ process.env.DEVHARNESS_DIR = previousDir;
35
+ initializePaths();
36
+ rmSync(dir, { recursive: true, force: true });
37
+ });
38
+ /** One request over the socket, resolving with the parsed reply. */
39
+ function callEndpoint(address, payload) {
40
+ return new Promise((resolve, reject) => {
41
+ let buffer = '';
42
+ const socket = connect(address);
43
+ socket.setEncoding('utf-8');
44
+ socket.on('connect', () => socket.write(JSON.stringify(payload) + '\n'));
45
+ socket.on('data', (chunk) => {
46
+ buffer += chunk;
47
+ const newline = buffer.indexOf('\n');
48
+ if (newline === -1)
49
+ return;
50
+ socket.destroy();
51
+ try {
52
+ resolve(JSON.parse(buffer.slice(0, newline)));
53
+ }
54
+ catch (error) {
55
+ reject(error);
56
+ }
57
+ });
58
+ socket.on('error', reject);
59
+ });
60
+ }
61
+ const IDENTITY = { pid: process.pid, ppid: process.ppid, cwd: '/repo' };
62
+ // ---------------------------------------------------------------------------
63
+ // addresses
64
+ // ---------------------------------------------------------------------------
65
+ describe('getEndpointAddress', () => {
66
+ posixOnly('puts the socket beside the presence record', () => {
67
+ expect(getEndpointAddress(1234)).toBe(join(getEndpointsDir(), '1234.sock'));
68
+ expect(getPresencePath(1234)).toBe(join(getEndpointsDir(), '1234.json'));
69
+ });
70
+ posixOnly('falls back to the temp directory when the path would exceed the sockaddr limit', () => {
71
+ process.env.DEVHARNESS_DIR = join(dir, 'x'.repeat(120));
72
+ initializePaths();
73
+ const address = getEndpointAddress(1234);
74
+ expect(address).not.toContain('x'.repeat(120));
75
+ expect(address).toContain('session-1234.sock');
76
+ });
77
+ });
78
+ // ---------------------------------------------------------------------------
79
+ // round trip
80
+ // ---------------------------------------------------------------------------
81
+ describe('endpoint round trip', () => {
82
+ posixOnly('runs the named tool and returns its response', async () => {
83
+ const calls = [];
84
+ endpoint = await startSessionEndpoint({
85
+ executeToolCall: async (tool, args) => {
86
+ calls.push({ tool, args });
87
+ return { content: [{ type: 'text', text: 'ran it' }], _meta: { tool } };
88
+ },
89
+ identity: IDENTITY,
90
+ });
91
+ const reply = await callEndpoint(endpoint.address, { tool: 'config', args: { action: 'status' } });
92
+ expect(reply.ok).toBe(true);
93
+ expect(reply.response.content[0].text).toBe('ran it');
94
+ expect(calls).toEqual([{ tool: 'config', args: { action: 'status' } }]);
95
+ });
96
+ posixOnly('returns a ToolError\'s response, so an error renders like any other result', async () => {
97
+ const failure = { content: [{ type: 'text', text: 'nope' }], isError: true, _errorId: 'SOME_ERROR' };
98
+ endpoint = await startSessionEndpoint({
99
+ executeToolCall: async () => { throw Object.assign(new Error('nope'), { response: failure }); },
100
+ identity: IDENTITY,
101
+ });
102
+ const reply = await callEndpoint(endpoint.address, { tool: 'whatever', args: {} });
103
+ expect(reply.ok).toBe(true);
104
+ expect(reply.response).toEqual(failure);
105
+ });
106
+ posixOnly('reports a throw that carries no response as a failed call', async () => {
107
+ endpoint = await startSessionEndpoint({
108
+ executeToolCall: async () => { throw new Error('Unknown tool: nope'); },
109
+ identity: IDENTITY,
110
+ });
111
+ const reply = await callEndpoint(endpoint.address, { tool: 'nope', args: {} });
112
+ expect(reply.ok).toBe(false);
113
+ expect(reply.error).toBe('Unknown tool: nope');
114
+ });
115
+ posixOnly('rejects a request that is not JSON', async () => {
116
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({}), identity: IDENTITY });
117
+ const reply = await new Promise((resolve, reject) => {
118
+ let buffer = '';
119
+ const socket = connect(endpoint.address);
120
+ socket.setEncoding('utf-8');
121
+ socket.on('connect', () => socket.write('not json\n'));
122
+ socket.on('data', (chunk) => {
123
+ buffer += chunk;
124
+ if (!buffer.includes('\n'))
125
+ return;
126
+ socket.destroy();
127
+ resolve(JSON.parse(buffer.split('\n')[0]));
128
+ });
129
+ socket.on('error', reject);
130
+ });
131
+ expect(reply.ok).toBe(false);
132
+ expect(reply.error).toMatch(/single line of JSON/);
133
+ });
134
+ posixOnly('rejects a request with no tool name', async () => {
135
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({}), identity: IDENTITY });
136
+ const reply = await callEndpoint(endpoint.address, { args: {} });
137
+ expect(reply.ok).toBe(false);
138
+ expect(reply.error).toMatch(/tool name/);
139
+ });
140
+ posixOnly('binds over a socket file left by a process that was killed', async () => {
141
+ mkdirSync(getEndpointsDir(), { recursive: true });
142
+ writeFileSync(getEndpointAddress(process.pid), '');
143
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({ content: [] }), identity: IDENTITY });
144
+ expect(endpoint).not.toBeNull();
145
+ const reply = await callEndpoint(endpoint.address, { tool: 'x', args: {} });
146
+ expect(reply.ok).toBe(true);
147
+ });
148
+ posixOnly('gives the socket and the record owner-only permissions', async () => {
149
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({ content: [] }), identity: IDENTITY });
150
+ expect(statSync(endpoint.address).mode & 0o777).toBe(0o600);
151
+ expect(statSync(endpoint.presencePath).mode & 0o777).toBe(0o600);
152
+ });
153
+ });
154
+ // ---------------------------------------------------------------------------
155
+ // presence records
156
+ // ---------------------------------------------------------------------------
157
+ describe('presence records', () => {
158
+ posixOnly('publishes this session and picks it up again', async () => {
159
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({}), identity: IDENTITY });
160
+ const records = listSessionRecords();
161
+ expect(records).toHaveLength(1);
162
+ expect(records[0].pid).toBe(process.pid);
163
+ expect(records[0].address).toBe(endpoint.address);
164
+ expect(records[0].shortId).toBeUndefined();
165
+ });
166
+ posixOnly('carries the ids once refresh supplies them', async () => {
167
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({}), identity: IDENTITY });
168
+ await endpoint.refresh({ sessionId: 'full-uuid', shortId: 'abcd1234' });
169
+ expect(listSessionRecords()[0].shortId).toBe('abcd1234');
170
+ expect(listSessionRecords()[0].sessionId).toBe('full-uuid');
171
+ });
172
+ posixOnly('drops a record whose process is gone, and its socket with it', async () => {
173
+ mkdirSync(getEndpointsDir(), { recursive: true });
174
+ const deadPid = 2; // pid 2 is not a devharness session on any machine this runs on
175
+ const deadSocket = join(getEndpointsDir(), '999999.sock');
176
+ writeFileSync(deadSocket, '');
177
+ writeFileSync(join(getEndpointsDir(), '999999.json'), JSON.stringify({
178
+ pid: 999999, ppid: deadPid, cwd: '/gone', address: deadSocket, startedAt: 1,
179
+ }));
180
+ expect(listSessionRecords()).toEqual([]);
181
+ expect(existsSync(join(getEndpointsDir(), '999999.json'))).toBe(false);
182
+ expect(existsSync(deadSocket)).toBe(false);
183
+ });
184
+ posixOnly('ignores a file that is not one of ours', async () => {
185
+ mkdirSync(getEndpointsDir(), { recursive: true });
186
+ // The shape server-claims.ts writes - no address, so nothing to dial.
187
+ writeFileSync(join(getEndpointsDir(), '4242.json'), JSON.stringify({
188
+ supervisorPid: 4242, cwd: '/repo', childPid: 4243,
189
+ }));
190
+ expect(listSessionRecords()).toEqual([]);
191
+ expect(existsSync(join(getEndpointsDir(), '4242.json'))).toBe(true);
192
+ });
193
+ posixOnly('removes the record and the socket on close', async () => {
194
+ endpoint = await startSessionEndpoint({ executeToolCall: async () => ({}), identity: IDENTITY });
195
+ const { address, presencePath } = endpoint;
196
+ await endpoint.close();
197
+ endpoint = null;
198
+ expect(existsSync(address)).toBe(false);
199
+ expect(existsSync(presencePath)).toBe(false);
200
+ });
201
+ });
202
+ //# sourceMappingURL=session-endpoint.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-endpoint.test.js","sourceRoot":"","sources":["../src/session-endpoint.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * One event stream per session.
3
+ *
4
+ * Every push event devharness produces - a guard block, an incoming message -
5
+ * appends one JSON line to `~/.devharness/events/<session>.jsonl`. One file
6
+ * means one watch: a session arms a single Monitor and receives every kind of
7
+ * event, including kinds added later, instead of one watch per feature.
8
+ *
9
+ * The plugin's SessionStart hook names this path to each session as it starts,
10
+ * which is what gets the watch armed. Nothing here verifies that a watch
11
+ * exists: a session with none simply receives nothing, the same as before the
12
+ * stream existed.
13
+ */
14
+ export type EventKind = 'block' | 'message';
15
+ export interface SessionEvent {
16
+ ts: string;
17
+ kind: EventKind;
18
+ [key: string]: unknown;
19
+ }
20
+ export declare function getEventsDir(): string;
21
+ export declare function getEventStreamPath(sessionName: string): string;
22
+ /**
23
+ * Append one event. Failures are logged and swallowed: the stream is a
24
+ * notification path, and losing a line must not fail the operation that
25
+ * produced it.
26
+ */
27
+ export declare function appendEvent(sessionName: string, kind: EventKind, payload: Record<string, unknown>): Promise<void>;
28
+ //# sourceMappingURL=session-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../src/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAQf"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * One event stream per session.
3
+ *
4
+ * Every push event devharness produces - a guard block, an incoming message -
5
+ * appends one JSON line to `~/.devharness/events/<session>.jsonl`. One file
6
+ * means one watch: a session arms a single Monitor and receives every kind of
7
+ * event, including kinds added later, instead of one watch per feature.
8
+ *
9
+ * The plugin's SessionStart hook names this path to each session as it starts,
10
+ * which is what gets the watch armed. Nothing here verifies that a watch
11
+ * exists: a session with none simply receives nothing, the same as before the
12
+ * stream existed.
13
+ */
14
+ import { promises as fs } from 'fs';
15
+ import { join } from 'path';
16
+ import { getOutputPath } from './helpers/paths.js';
17
+ export function getEventsDir() {
18
+ return getOutputPath('events', { global: true });
19
+ }
20
+ export function getEventStreamPath(sessionName) {
21
+ return join(getEventsDir(), `${sessionName}.jsonl`);
22
+ }
23
+ /**
24
+ * Append one event. Failures are logged and swallowed: the stream is a
25
+ * notification path, and losing a line must not fail the operation that
26
+ * produced it.
27
+ */
28
+ export async function appendEvent(sessionName, kind, payload) {
29
+ const event = { ts: new Date().toISOString(), kind, ...payload };
30
+ try {
31
+ await fs.mkdir(getEventsDir(), { recursive: true });
32
+ await fs.appendFile(getEventStreamPath(sessionName), JSON.stringify(event) + '\n');
33
+ }
34
+ catch (error) {
35
+ console.error(`[devharness] Failed to append ${kind} event: ${error}`);
36
+ }
37
+ }
38
+ //# sourceMappingURL=session-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-events.js","sourceRoot":"","sources":["../src/session-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAUnD,MAAM,UAAU,YAAY;IAC1B,OAAO,aAAa,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IACpD,OAAO,IAAI,CAAC,YAAY,EAAE,EAAE,GAAG,WAAW,QAAQ,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,WAAmB,EACnB,IAAe,EACf,OAAgC;IAEhC,MAAM,KAAK,GAAiB,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/E,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IACrF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,iCAAiC,IAAI,WAAW,KAAK,EAAE,CAAC,CAAC;IACzE,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tests for the per-session event stream.
3
+ *
4
+ * The property worth pinning is that every kind lands in one file: a session
5
+ * arms one watch, and a kind added later arrives on it without a second one.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=session-events.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-events.test.d.ts","sourceRoot":"","sources":["../src/session-events.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Tests for the per-session event stream.
3
+ *
4
+ * The property worth pinning is that every kind lands in one file: a session
5
+ * arms one watch, and a kind added later arrives on it without a second one.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs';
9
+ import { tmpdir } from 'os';
10
+ import { join } from 'path';
11
+ import { initializePaths } from './helpers/paths.js';
12
+ import { appendEvent, getEventStreamPath, getEventsDir, } from './session-events.js';
13
+ let dir;
14
+ let previousDir;
15
+ const SESSION = 'aaaaaaaa';
16
+ function readStream() {
17
+ const path = getEventStreamPath(SESSION);
18
+ if (!existsSync(path))
19
+ return [];
20
+ return readFileSync(path, 'utf-8')
21
+ .split('\n')
22
+ .filter(line => line.trim())
23
+ .map(line => JSON.parse(line));
24
+ }
25
+ beforeEach(() => {
26
+ previousDir = process.env.DEVHARNESS_DIR;
27
+ dir = mkdtempSync(join(tmpdir(), 'devharness-events-'));
28
+ process.env.DEVHARNESS_DIR = dir;
29
+ initializePaths();
30
+ });
31
+ afterEach(() => {
32
+ if (previousDir === undefined)
33
+ delete process.env.DEVHARNESS_DIR;
34
+ else
35
+ process.env.DEVHARNESS_DIR = previousDir;
36
+ initializePaths();
37
+ rmSync(dir, { recursive: true, force: true });
38
+ });
39
+ describe('appendEvent', () => {
40
+ it('writes one line per event, each stamped and kinded', async () => {
41
+ await appendEvent(SESSION, 'block', { guard: 'port', tool: 'navigate' });
42
+ await appendEvent(SESSION, 'message', { from: 'bbbbbbbb' });
43
+ const events = readStream();
44
+ expect(events.map(e => e.kind)).toEqual(['block', 'message']);
45
+ expect(events[0]).toMatchObject({ kind: 'block', guard: 'port', tool: 'navigate' });
46
+ expect(typeof events[0].ts).toBe('string');
47
+ });
48
+ it('puts every kind in the one file, so one watch covers them all', async () => {
49
+ await appendEvent(SESSION, 'block', {});
50
+ await appendEvent(SESSION, 'message', {});
51
+ await appendEvent(SESSION, 'block', {});
52
+ expect(getEventStreamPath(SESSION)).toBe(join(getEventsDir(), 'aaaaaaaa.jsonl'));
53
+ expect(readStream()).toHaveLength(3);
54
+ });
55
+ });
56
+ //# sourceMappingURL=session-events.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-events.test.js","sourceRoot":"","sources":["../src/session-events.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Which Claude Code session this process serves.
3
+ *
4
+ * Claude Code exports CLAUDE_CODE_SESSION_ID into the environment it spawns
5
+ * the supervisor with, and the child inherits it. That value is present from
6
+ * the first instruction and unchanged when the supervisor replaces the child,
7
+ * so anything naming this session on disk - a mailbox filename, a presence
8
+ * record - keeps the same name across a rebuild.
9
+ *
10
+ * The session detector produces the same id, but only after a tool response
11
+ * carrying a `pid:` footer has been written into the session's .jsonl and
12
+ * matched. A session driven from the CLI writes no such footer, and a child
13
+ * restarted by a rebuild has not written one yet.
14
+ */
15
+ /** The full session id from the environment, or undefined when this process
16
+ * was not spawned by Claude Code. */
17
+ export declare function getClaudeSessionId(): string | undefined;
18
+ /** The first 8 characters of the session id - the form the dashboard and the
19
+ * session detector both display. */
20
+ export declare function getClaudeShortId(): string | undefined;
21
+ /**
22
+ * The name this session is known by on disk - its mailbox filename, its event
23
+ * stream filename, its presence record.
24
+ *
25
+ * The hook's record comes first: it is the only source that follows the
26
+ * conversation through a `/clear`, which changes the id without restarting this
27
+ * process. The environment is next, correct from the first instruction and for
28
+ * as long as the conversation lasts. The detector covers a client that exports
29
+ * no session id. The pid form is last, and changes on every restart, which is
30
+ * why it is the fallback rather than the default.
31
+ */
32
+ export declare function resolveSessionName(detectedShortId?: string): string;
33
+ /**
34
+ * The Claude process this server belongs to.
35
+ *
36
+ * `CLAUDE_CODE_MESSAGING_SOCKET` carries that pid in its filename, and it is
37
+ * present in both the hook's environment and the server's, under `-p` and
38
+ * interactively. `CLAUDE_PID` carries it more directly and is absent from the
39
+ * server under `-p`, so the socket path is the derivation that works on both
40
+ * sides.
41
+ */
42
+ export declare function getClientPid(): number | undefined;
43
+ /** Where the plugin's SessionStart hook records the conversation a client is
44
+ * currently on. */
45
+ export declare function getClientsDir(): string;
46
+ export declare function getClientRecordPath(clientPid: number): string;
47
+ /**
48
+ * The conversation this client is on now, as last written by the hook.
49
+ *
50
+ * `CLAUDE_CODE_SESSION_ID` is read once when the process is spawned, and
51
+ * `/clear` gives the conversation a new id without restarting it. The hook runs
52
+ * on every start reason including `clear`, carrying the current id, so this
53
+ * file is the only source that tracks the change while the process lives.
54
+ */
55
+ export declare function readCurrentSessionId(): string | undefined;
56
+ //# sourceMappingURL=session-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-identity.d.ts","sourceRoot":"","sources":["../src/session-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH;sCACsC;AACtC,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAIvD;AAED;qCACqC;AACrC,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAErD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAKjD;AAED;oBACoB;AACpB,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAUzD"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Which Claude Code session this process serves.
3
+ *
4
+ * Claude Code exports CLAUDE_CODE_SESSION_ID into the environment it spawns
5
+ * the supervisor with, and the child inherits it. That value is present from
6
+ * the first instruction and unchanged when the supervisor replaces the child,
7
+ * so anything naming this session on disk - a mailbox filename, a presence
8
+ * record - keeps the same name across a rebuild.
9
+ *
10
+ * The session detector produces the same id, but only after a tool response
11
+ * carrying a `pid:` footer has been written into the session's .jsonl and
12
+ * matched. A session driven from the CLI writes no such footer, and a child
13
+ * restarted by a rebuild has not written one yet.
14
+ */
15
+ import { readFileSync } from 'fs';
16
+ import { join, basename } from 'path';
17
+ import { getOutputPath } from './helpers/paths.js';
18
+ /** Ids become filenames; anything outside this set could escape the directory
19
+ * holding them. */
20
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
21
+ /** The full session id from the environment, or undefined when this process
22
+ * was not spawned by Claude Code. */
23
+ export function getClaudeSessionId() {
24
+ const value = process.env.CLAUDE_CODE_SESSION_ID;
25
+ if (!value || !SESSION_ID_PATTERN.test(value))
26
+ return undefined;
27
+ return value;
28
+ }
29
+ /** The first 8 characters of the session id - the form the dashboard and the
30
+ * session detector both display. */
31
+ export function getClaudeShortId() {
32
+ return getClaudeSessionId()?.slice(0, 8);
33
+ }
34
+ /**
35
+ * The name this session is known by on disk - its mailbox filename, its event
36
+ * stream filename, its presence record.
37
+ *
38
+ * The hook's record comes first: it is the only source that follows the
39
+ * conversation through a `/clear`, which changes the id without restarting this
40
+ * process. The environment is next, correct from the first instruction and for
41
+ * as long as the conversation lasts. The detector covers a client that exports
42
+ * no session id. The pid form is last, and changes on every restart, which is
43
+ * why it is the fallback rather than the default.
44
+ */
45
+ export function resolveSessionName(detectedShortId) {
46
+ return readCurrentSessionId()?.slice(0, 8)
47
+ ?? getClaudeShortId()
48
+ ?? detectedShortId
49
+ ?? `pid-${process.pid}`;
50
+ }
51
+ /**
52
+ * The Claude process this server belongs to.
53
+ *
54
+ * `CLAUDE_CODE_MESSAGING_SOCKET` carries that pid in its filename, and it is
55
+ * present in both the hook's environment and the server's, under `-p` and
56
+ * interactively. `CLAUDE_PID` carries it more directly and is absent from the
57
+ * server under `-p`, so the socket path is the derivation that works on both
58
+ * sides.
59
+ */
60
+ export function getClientPid() {
61
+ const socket = process.env.CLAUDE_CODE_MESSAGING_SOCKET;
62
+ if (!socket)
63
+ return undefined;
64
+ const pid = Number(basename(socket).replace(/\.sock$/, ''));
65
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
66
+ }
67
+ /** Where the plugin's SessionStart hook records the conversation a client is
68
+ * currently on. */
69
+ export function getClientsDir() {
70
+ return getOutputPath('clients', { global: true });
71
+ }
72
+ export function getClientRecordPath(clientPid) {
73
+ return join(getClientsDir(), `${clientPid}.json`);
74
+ }
75
+ /**
76
+ * The conversation this client is on now, as last written by the hook.
77
+ *
78
+ * `CLAUDE_CODE_SESSION_ID` is read once when the process is spawned, and
79
+ * `/clear` gives the conversation a new id without restarting it. The hook runs
80
+ * on every start reason including `clear`, carrying the current id, so this
81
+ * file is the only source that tracks the change while the process lives.
82
+ */
83
+ export function readCurrentSessionId() {
84
+ const clientPid = getClientPid();
85
+ if (clientPid === undefined)
86
+ return undefined;
87
+ try {
88
+ const record = JSON.parse(readFileSync(getClientRecordPath(clientPid), 'utf-8'));
89
+ const id = record?.sessionId;
90
+ return typeof id === 'string' && SESSION_ID_PATTERN.test(id) ? id : undefined;
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ //# sourceMappingURL=session-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-identity.js","sourceRoot":"","sources":["../src/session-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;oBACoB;AACpB,MAAM,kBAAkB,GAAG,oCAAoC,CAAC;AAEhE;sCACsC;AACtC,MAAM,UAAU,kBAAkB;IAChC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACjD,IAAI,CAAC,KAAK,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAChE,OAAO,KAAK,CAAC;AACf,CAAC;AAED;qCACqC;AACrC,MAAM,UAAU,gBAAgB;IAC9B,OAAO,kBAAkB,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,eAAwB;IACzD,OAAO,oBAAoB,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;WACrC,gBAAgB,EAAE;WAClB,eAAe;WACf,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC;IACxD,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5D,CAAC;AAED;oBACoB;AACpB,MAAM,UAAU,aAAa;IAC3B,OAAO,aAAa,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACnD,OAAO,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,SAAS,OAAO,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,mBAAmB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACjF,MAAM,EAAE,GAAG,MAAM,EAAE,SAAS,CAAC;QAC7B,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tests for reading the Claude Code session id out of the environment.
3
+ *
4
+ * The id becomes a filename, so a value that could traverse out of the
5
+ * directory holding mailboxes and presence records is rejected rather than
6
+ * sanitised - a sanitised name would silently address a different session.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=session-identity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-identity.test.d.ts","sourceRoot":"","sources":["../src/session-identity.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}