pendingyou 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +50 -15
- package/dist/api.js +27 -0
- package/dist/apps/claude-code.js +14 -11
- package/dist/apps/codex-wake.js +1 -1
- package/dist/apps/codex.js +102 -32
- package/dist/apps/opencode-plugin.js +52 -2
- package/dist/apps/opencode.js +13 -10
- package/dist/apps/pi-extension.js +41 -1
- package/dist/apps/pi.js +13 -10
- package/dist/apps/switch.js +15 -0
- package/dist/args.js +65 -1
- package/dist/claude.js +6 -2
- package/dist/format.js +29 -0
- package/dist/handed.js +28 -12
- package/dist/hooks.js +2 -1
- package/dist/io.js +1 -0
- package/dist/main.js +26 -5
- package/dist/permission.js +117 -36
- package/dist/pickup.js +16 -5
- package/dist/presence.js +330 -0
- package/dist/stopcheck.js +4 -4
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +41 -3
- package/mod/hooks/wake.js +71 -2
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
export const PRESENCE_EVERY_MS = 5 * 60_000;
|
|
8
9
|
const PRODUCTION = 'https://www.pendingyou.com';
|
|
9
10
|
const HOOK_MS = 5000;
|
|
10
11
|
const TURN_MESSAGES = 200;
|
|
@@ -43,7 +44,7 @@ export function partId(now = Date.now()) {
|
|
|
43
44
|
const random = [...randomBytes(14)].map((byte) => BASE62[byte % 62]).join('');
|
|
44
45
|
return `prt_${time}${random}`;
|
|
45
46
|
}
|
|
46
|
-
function viaShim(config, origin) {
|
|
47
|
+
function viaShim(config, origin, directory) {
|
|
47
48
|
const shim = join(config, 'bin', 'pendingyou-hook');
|
|
48
49
|
const args = (sub) => [
|
|
49
50
|
sub,
|
|
@@ -112,6 +113,29 @@ function viaShim(config, origin) {
|
|
|
112
113
|
}
|
|
113
114
|
},
|
|
114
115
|
partId: () => partId(),
|
|
116
|
+
presence: (session, state) => {
|
|
117
|
+
try {
|
|
118
|
+
const child = spawn(shim, [
|
|
119
|
+
...args('presence'),
|
|
120
|
+
'--state',
|
|
121
|
+
state,
|
|
122
|
+
'--session',
|
|
123
|
+
session,
|
|
124
|
+
'--cwd',
|
|
125
|
+
directory,
|
|
126
|
+
'--at',
|
|
127
|
+
String(Date.now()),
|
|
128
|
+
], { detached: true, stdio: 'ignore' });
|
|
129
|
+
child.on('error', () => { });
|
|
130
|
+
child.unref();
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
},
|
|
134
|
+
every: (ms, run) => {
|
|
135
|
+
const timer = setInterval(run, ms);
|
|
136
|
+
timer.unref?.();
|
|
137
|
+
return () => clearInterval(timer);
|
|
138
|
+
},
|
|
115
139
|
wanted: (session) => {
|
|
116
140
|
try {
|
|
117
141
|
return wantsListener(JSON.parse(readFileSync(join(config, 'opencode-threads.json'), 'utf8')), session);
|
|
@@ -129,7 +153,7 @@ export const PendingYou = async (input) => {
|
|
|
129
153
|
const origin = isObject(manifest) && typeof manifest.origin === 'string' ? manifest.origin : null;
|
|
130
154
|
if (!origin)
|
|
131
155
|
return {};
|
|
132
|
-
return pendingYou(input, viaShim(config, origin));
|
|
156
|
+
return pendingYou(input, viaShim(config, origin, input.directory));
|
|
133
157
|
}
|
|
134
158
|
catch {
|
|
135
159
|
return {};
|
|
@@ -138,6 +162,24 @@ export const PendingYou = async (input) => {
|
|
|
138
162
|
export async function pendingYou(input, deps) {
|
|
139
163
|
const { client, directory } = input;
|
|
140
164
|
const sessions = new Map();
|
|
165
|
+
const present = new Set();
|
|
166
|
+
let stopBeat = null;
|
|
167
|
+
const live = (id) => {
|
|
168
|
+
if (present.has(id) || !deps.presence)
|
|
169
|
+
return;
|
|
170
|
+
present.add(id);
|
|
171
|
+
deps.presence(id, 'live');
|
|
172
|
+
stopBeat ??=
|
|
173
|
+
deps.every?.(PRESENCE_EVERY_MS, () => {
|
|
174
|
+
for (const each of present)
|
|
175
|
+
deps.presence?.(each, 'live');
|
|
176
|
+
}) ?? null;
|
|
177
|
+
};
|
|
178
|
+
const closed = (id) => {
|
|
179
|
+
if (!present.delete(id))
|
|
180
|
+
return;
|
|
181
|
+
deps.presence?.(id, 'closed');
|
|
182
|
+
};
|
|
141
183
|
const known = async (id) => {
|
|
142
184
|
const found = sessions.get(id);
|
|
143
185
|
if (found && found.parent !== undefined)
|
|
@@ -328,6 +370,8 @@ export async function pendingYou(input, deps) {
|
|
|
328
370
|
session.fresh = true;
|
|
329
371
|
session.parent = typeof info.parentID === 'string' && info.parentID ? info.parentID : null;
|
|
330
372
|
sessions.set(info.id, session);
|
|
373
|
+
if (!session.parent)
|
|
374
|
+
live(info.id);
|
|
331
375
|
}
|
|
332
376
|
else if (event.type === 'session.deleted' && isObject(properties.info)) {
|
|
333
377
|
const id = properties.info.id;
|
|
@@ -338,6 +382,7 @@ export async function pendingYou(input, deps) {
|
|
|
338
382
|
deleted.gone = true;
|
|
339
383
|
deleted?.listener?.stop();
|
|
340
384
|
sessions.delete(id);
|
|
385
|
+
closed(id);
|
|
341
386
|
}
|
|
342
387
|
else if (event.type === 'session.status' &&
|
|
343
388
|
typeof properties.sessionID === 'string' &&
|
|
@@ -353,6 +398,7 @@ export async function pendingYou(input, deps) {
|
|
|
353
398
|
const session = await known(id);
|
|
354
399
|
if (session.parent)
|
|
355
400
|
return;
|
|
401
|
+
live(id);
|
|
356
402
|
if (typeof output.message.id === 'string')
|
|
357
403
|
session.turn = output.message.id;
|
|
358
404
|
if (output.parts.some((part) => ourMark(part) !== null))
|
|
@@ -409,6 +455,10 @@ export async function pendingYou(input, deps) {
|
|
|
409
455
|
session.gone = true;
|
|
410
456
|
session.listener?.stop();
|
|
411
457
|
}
|
|
458
|
+
stopBeat?.();
|
|
459
|
+
stopBeat = null;
|
|
460
|
+
for (const id of [...present])
|
|
461
|
+
closed(id);
|
|
412
462
|
},
|
|
413
463
|
};
|
|
414
464
|
}
|
package/dist/apps/opencode.js
CHANGED
|
@@ -8,11 +8,13 @@ import { configDir, readJson, readText, writeWhole } from "../files.js";
|
|
|
8
8
|
import { originArgs } from "../hooks.js";
|
|
9
9
|
import { cliPackage } from "../install.js";
|
|
10
10
|
import { JsoncError, jsonOf, nodeAt, parse, removeAt, setAt } from "../jsonc.js";
|
|
11
|
+
import { removePresenceFiles } from "../presence.js";
|
|
11
12
|
import { foundBy, headlessReason } from "../remote.js";
|
|
12
13
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
13
14
|
import { VERSION } from "../version.js";
|
|
14
15
|
import { threadFiles } from "./codex-wake.js";
|
|
15
16
|
import { APP_NAMES } from "./ids.js";
|
|
17
|
+
import { switches } from "./switch.js";
|
|
16
18
|
const NAME = APP_NAMES.opencode;
|
|
17
19
|
const SCHEMA = 'https://opencode.ai/config.json';
|
|
18
20
|
export const OPENCODE_MIN = '1.17.0';
|
|
@@ -170,15 +172,12 @@ async function sharedSkill(io) {
|
|
|
170
172
|
return place;
|
|
171
173
|
return null;
|
|
172
174
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const answer = await io.ask('Switch OpenCode to this computer’s sign-in? [Y/n] ');
|
|
180
|
-
return !/^\s*no?\s*$/i.test(answer);
|
|
181
|
-
}
|
|
175
|
+
const switchesFor = (io, ctx) => switches(io, ctx, {
|
|
176
|
+
name: 'OpenCode',
|
|
177
|
+
what: 'OpenCode signs in to Pending You by itself here (opencode mcp auth).',
|
|
178
|
+
uses: 'its plugin uses',
|
|
179
|
+
headless: true,
|
|
180
|
+
});
|
|
182
181
|
const writeConfig = (path, text) => mkdir(dirname(path), { recursive: true })
|
|
183
182
|
.then(() => writeWhole(path, text))
|
|
184
183
|
.then(() => true, () => false);
|
|
@@ -347,7 +346,7 @@ export const opencode = {
|
|
|
347
346
|
else if (server?.kind === 'remote') {
|
|
348
347
|
if (mode === 'oauth')
|
|
349
348
|
action = 'keep';
|
|
350
|
-
else if (await
|
|
349
|
+
else if (await switchesFor(io, ctx))
|
|
351
350
|
action = 'write';
|
|
352
351
|
else {
|
|
353
352
|
mode = 'oauth';
|
|
@@ -554,6 +553,7 @@ export const opencode = {
|
|
|
554
553
|
steps.push(skill);
|
|
555
554
|
for (const path of threadFiles(io, 'opencode'))
|
|
556
555
|
await rm(path, { recursive: true, force: true });
|
|
556
|
+
await removePresenceFiles(io, 'opencode');
|
|
557
557
|
await rm(manifestPath(io), { force: true });
|
|
558
558
|
if (steps.length === 0)
|
|
559
559
|
steps.push({ ok: true, text: 'Nothing of Pending You’s was in OpenCode.' });
|
|
@@ -648,6 +648,9 @@ export const opencode = {
|
|
|
648
648
|
...(bridgeLine ? [` ${mark(bridgeOk)} Bridge: ${bridgeLine}`] : []),
|
|
649
649
|
` ${mark(pluginOk)} Plugin: ${pluginLine}`,
|
|
650
650
|
` ${mark(own || shared !== null)} Skill: ${own ? `saved (${opencodeSkillPath(io)})` : shared ? `OpenCode reads the one at ${shared}` : `not saved; run ${init}`}`,
|
|
651
|
+
connected && pluginOk
|
|
652
|
+
? ' ok Presence: tells Pending You when this session is open (the plugin says so every 5 minutes while OpenCode runs)'
|
|
653
|
+
: ` Presence: off: it needs ${connected ? 'the plugin' : 'OpenCode signed in through this computer’s own connection'}`,
|
|
651
654
|
];
|
|
652
655
|
const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
|
|
653
656
|
if (setup)
|
|
@@ -11,6 +11,12 @@ const STOP_MS = 2000;
|
|
|
11
11
|
export const CARD_TOOL = /^mcp__[A-Za-z0-9_]*pendingyou[A-Za-z0-9_]*__(post_request|update_request|reply_in_thread|cancel_request|answer_delegated|hand_back)$/i;
|
|
12
12
|
export const HANDED_FOR_MS = 12 * 60 * 60_000;
|
|
13
13
|
export const HANDED_PREFIX = 'Pending You: your person handed you';
|
|
14
|
+
const HANDED_REPLY = ' on the question handed to you, ';
|
|
15
|
+
const isHandedWake = (text) => {
|
|
16
|
+
const first = text.trimStart().split('\n')[0] ?? '';
|
|
17
|
+
return (first.startsWith(HANDED_PREFIX) ||
|
|
18
|
+
(first.startsWith('Pending You: ') && first.includes(HANDED_REPLY)));
|
|
19
|
+
};
|
|
14
20
|
const SOURCES = {
|
|
15
21
|
startup: 'startup',
|
|
16
22
|
reload: 'startup',
|
|
@@ -24,6 +30,7 @@ export const SETUP_LINES = [
|
|
|
24
30
|
'Pending You: your person answered setup’s test question',
|
|
25
31
|
'Pending You: this computer is connected (',
|
|
26
32
|
];
|
|
33
|
+
export const PRESENCE_EVERY_MS = 5 * 60_000;
|
|
27
34
|
const MAX_STRING = 20_000;
|
|
28
35
|
const SESSION = /^[A-Za-z0-9][A-Za-z0-9-]{0,99}$/;
|
|
29
36
|
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -146,6 +153,27 @@ export function pendingYou(setup, options = {}) {
|
|
|
146
153
|
let listener = null;
|
|
147
154
|
let again = false;
|
|
148
155
|
const handedWakes = new Set();
|
|
156
|
+
let present = '';
|
|
157
|
+
let beat = null;
|
|
158
|
+
const presence = (id, state) => {
|
|
159
|
+
if (!id)
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
const child = spawn(setup.shim, args('presence', '--state', state, '--session', id, '--at', String(Date.now()), ...(cwd ? ['--cwd', cwd] : [])), { detached: true, stdio: 'ignore' });
|
|
163
|
+
child.on('error', () => { });
|
|
164
|
+
unref(child);
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
167
|
+
};
|
|
168
|
+
const heartbeat = () => {
|
|
169
|
+
beat = later(() => {
|
|
170
|
+
if (closed || !present)
|
|
171
|
+
return;
|
|
172
|
+
presence(present, 'live');
|
|
173
|
+
heartbeat();
|
|
174
|
+
}, PRESENCE_EVERY_MS);
|
|
175
|
+
unref(beat);
|
|
176
|
+
};
|
|
149
177
|
const hook = (command, input) => new Promise((resolve) => {
|
|
150
178
|
let out = '';
|
|
151
179
|
let done = false;
|
|
@@ -249,7 +277,7 @@ export function pendingYou(setup, options = {}) {
|
|
|
249
277
|
for (let end = buffer.indexOf('\n'); end !== -1; end = buffer.indexOf('\n')) {
|
|
250
278
|
const message = wakeOf(buffer.slice(0, end));
|
|
251
279
|
buffer = buffer.slice(end + 1);
|
|
252
|
-
if (message
|
|
280
|
+
if (message && isHandedWake(message)) {
|
|
253
281
|
if (handedWakes.size >= 20)
|
|
254
282
|
handedWakes.clear();
|
|
255
283
|
handedWakes.add(message.trim());
|
|
@@ -281,6 +309,14 @@ export function pendingYou(setup, options = {}) {
|
|
|
281
309
|
session = '';
|
|
282
310
|
}
|
|
283
311
|
cwd = ctx.cwd;
|
|
312
|
+
if (session && session !== present) {
|
|
313
|
+
if (present)
|
|
314
|
+
presence(present, 'closed');
|
|
315
|
+
present = session;
|
|
316
|
+
presence(session, 'live');
|
|
317
|
+
cancel(beat);
|
|
318
|
+
heartbeat();
|
|
319
|
+
}
|
|
284
320
|
const said = await hook('pickup', {
|
|
285
321
|
cwd,
|
|
286
322
|
session_id: session,
|
|
@@ -356,6 +392,10 @@ export function pendingYou(setup, options = {}) {
|
|
|
356
392
|
});
|
|
357
393
|
pi.on('session_shutdown', async () => {
|
|
358
394
|
closed = true;
|
|
395
|
+
cancel(beat);
|
|
396
|
+
if (present)
|
|
397
|
+
presence(present, 'closed');
|
|
398
|
+
present = '';
|
|
359
399
|
const child = listener;
|
|
360
400
|
listener = null;
|
|
361
401
|
if (!child || child.exitCode !== null || child.signalCode !== null)
|
package/dist/apps/pi.js
CHANGED
|
@@ -8,12 +8,14 @@ import { originArgs } from "../hooks.js";
|
|
|
8
8
|
import { cliPackage } from "../install.js";
|
|
9
9
|
import { editJson, isObject, readObject } from "../json.js";
|
|
10
10
|
import { atLeast } from "../mod.js";
|
|
11
|
+
import { removePresenceFiles } from "../presence.js";
|
|
11
12
|
import { foundBy, headlessReason } from "../remote.js";
|
|
12
13
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
13
14
|
import { shimPath } from "../shim.js";
|
|
14
15
|
import { VERSION } from "../version.js";
|
|
15
16
|
import { threadFiles } from "./codex-wake.js";
|
|
16
17
|
import { APP_NAMES } from "./ids.js";
|
|
18
|
+
import { switches } from "./switch.js";
|
|
17
19
|
const NAME = APP_NAMES.pi;
|
|
18
20
|
export const PI_MIN = '0.99.0';
|
|
19
21
|
export const PI_CIMD = '1.0.1';
|
|
@@ -133,15 +135,12 @@ function entryFor(bridge, origin, version, before) {
|
|
|
133
135
|
...kept,
|
|
134
136
|
};
|
|
135
137
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const answer = await io.ask('Switch Pi to this computer’s sign-in? [Y/n] ');
|
|
143
|
-
return !/^\s*no?\s*$/i.test(answer);
|
|
144
|
-
}
|
|
138
|
+
const switchesFor = (io, ctx) => switches(io, ctx, {
|
|
139
|
+
name: 'Pi',
|
|
140
|
+
what: 'Pi signs in to Pending You by itself here (pi mcp login).',
|
|
141
|
+
uses: 'Pending You’s extension for Pi uses',
|
|
142
|
+
headless: false,
|
|
143
|
+
});
|
|
145
144
|
async function extensionSource(io, copy) {
|
|
146
145
|
const places = [
|
|
147
146
|
...(copy ? [join(cliPackage(copy.prefix), 'dist', 'apps', 'pi-extension.js')] : []),
|
|
@@ -289,7 +288,7 @@ export const pi = {
|
|
|
289
288
|
else if (server?.url) {
|
|
290
289
|
if (how === 'pi')
|
|
291
290
|
action = server.exposure === 'direct' ? 'keep' : 'expose';
|
|
292
|
-
else if (await
|
|
291
|
+
else if (await switchesFor(io, ctx))
|
|
293
292
|
action = 'write';
|
|
294
293
|
else {
|
|
295
294
|
how = 'pi';
|
|
@@ -502,6 +501,7 @@ export const pi = {
|
|
|
502
501
|
steps.push(skill);
|
|
503
502
|
for (const path of threadFiles(io, 'pi'))
|
|
504
503
|
await rm(path, { recursive: true, force: true });
|
|
504
|
+
await removePresenceFiles(io, 'pi');
|
|
505
505
|
await rm(manifestPath(io), { force: true });
|
|
506
506
|
return steps;
|
|
507
507
|
},
|
|
@@ -601,6 +601,9 @@ export const pi = {
|
|
|
601
601
|
...(bridgeLine ? [` ${mark(bridgeOk)} Bridge: ${bridgeLine}`] : []),
|
|
602
602
|
` ${mark(skillOk)} Skill: ${skillText}`,
|
|
603
603
|
` ${mark(extensionOk)} Extension: ${extensionText}`,
|
|
604
|
+
connected && extensionOk
|
|
605
|
+
? ' ok Presence: tells Pending You when this session is open (the extension says so every 5 minutes while Pi runs)'
|
|
606
|
+
: ` Presence: off: it needs ${connected ? 'the extension' : 'Pi signed in through this computer’s own connection'}`,
|
|
604
607
|
];
|
|
605
608
|
const setup = setupStatus(NAME, signIn.state === 'ok' ? setupOf(signIn.for) : null);
|
|
606
609
|
if (setup)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const movesOver = (name) => `Once ${name} shows up on this computer’s sign-in, Pending You moves the old sign-in’s agents over to it, with their cards and the questions you delegated to them, and signs the old one out.`;
|
|
2
|
+
export async function switches(io, ctx, options) {
|
|
3
|
+
const { name } = options;
|
|
4
|
+
if (ctx.yes)
|
|
5
|
+
return true;
|
|
6
|
+
if (options.headless && ctx.headless) {
|
|
7
|
+
io.out(`${options.what} There’s no browser here (${ctx.headless}), where that sign-in can’t finish, so init is switching ${name} to this computer’s own sign-in, which ${options.uses} too. ${movesOver(name)}\n`);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (!io.interactive)
|
|
11
|
+
return false;
|
|
12
|
+
io.out(`${options.what} init can move ${name} to this computer’s own sign-in, which ${options.uses} too: one sign-in for both. ${movesOver(name)}\n`);
|
|
13
|
+
const answer = await io.ask(`Switch ${name} to this computer’s sign-in? [Y/n] `);
|
|
14
|
+
return !/^\s*no?\s*$/i.test(answer);
|
|
15
|
+
}
|
package/dist/args.js
CHANGED
|
@@ -64,6 +64,7 @@ const COMMANDS = new Set([
|
|
|
64
64
|
'permission',
|
|
65
65
|
'permission-done',
|
|
66
66
|
'permission-card',
|
|
67
|
+
'presence',
|
|
67
68
|
]);
|
|
68
69
|
const FLAGS = {
|
|
69
70
|
help: {},
|
|
@@ -88,6 +89,18 @@ const FLAGS = {
|
|
|
88
89
|
permission: { origin: true, app: true },
|
|
89
90
|
'permission-done': { origin: true, app: true },
|
|
90
91
|
'permission-card': { origin: true, session: true, worker: true },
|
|
92
|
+
presence: {
|
|
93
|
+
origin: true,
|
|
94
|
+
app: true,
|
|
95
|
+
state: true,
|
|
96
|
+
session: true,
|
|
97
|
+
cwd: true,
|
|
98
|
+
name: true,
|
|
99
|
+
transcript: true,
|
|
100
|
+
keep: false,
|
|
101
|
+
pid: true,
|
|
102
|
+
at: true,
|
|
103
|
+
},
|
|
91
104
|
init: {
|
|
92
105
|
origin: true,
|
|
93
106
|
'no-login': false,
|
|
@@ -107,7 +120,20 @@ const FLAGS = {
|
|
|
107
120
|
'mcp-headers': { origin: true, check: false, app: true },
|
|
108
121
|
mcp: { origin: true, app: true, check: false },
|
|
109
122
|
};
|
|
110
|
-
const VALUE_OPTIONS = new Set([
|
|
123
|
+
const VALUE_OPTIONS = new Set([
|
|
124
|
+
'origin',
|
|
125
|
+
'timeout',
|
|
126
|
+
'name',
|
|
127
|
+
'app',
|
|
128
|
+
'thread',
|
|
129
|
+
'session',
|
|
130
|
+
'worker',
|
|
131
|
+
'state',
|
|
132
|
+
'cwd',
|
|
133
|
+
'transcript',
|
|
134
|
+
'pid',
|
|
135
|
+
'at',
|
|
136
|
+
]);
|
|
111
137
|
function splitCommand(argv) {
|
|
112
138
|
let index = 0;
|
|
113
139
|
while (index < argv.length) {
|
|
@@ -128,6 +154,7 @@ export const HOOK_COMMANDS = new Set([
|
|
|
128
154
|
'posted',
|
|
129
155
|
'permission',
|
|
130
156
|
'permission-done',
|
|
157
|
+
'presence',
|
|
131
158
|
]);
|
|
132
159
|
const THREAD = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
|
|
133
160
|
function appsOf(text) {
|
|
@@ -301,6 +328,43 @@ export function parseArgs(argv, env = {}) {
|
|
|
301
328
|
throw new UsageError('permission-card needs --session <id> and --worker <id>.');
|
|
302
329
|
return { name: 'permission-card', origin, session, worker };
|
|
303
330
|
}
|
|
331
|
+
case 'presence': {
|
|
332
|
+
none();
|
|
333
|
+
const state = text('state');
|
|
334
|
+
if (state !== undefined && state !== 'live' && state !== 'closed')
|
|
335
|
+
throw new UsageError('--state is live or closed.');
|
|
336
|
+
const session = text('session');
|
|
337
|
+
if (session !== undefined && !THREAD.test(session))
|
|
338
|
+
throw new UsageError('presence needs a session id, like: --session 0199a1b2-…');
|
|
339
|
+
const number = (key) => {
|
|
340
|
+
const given = text(key);
|
|
341
|
+
if (given === undefined)
|
|
342
|
+
return undefined;
|
|
343
|
+
if (!/^\d{1,16}$/.test(given))
|
|
344
|
+
throw new UsageError(`--${key} needs a number.`);
|
|
345
|
+
return Number(given);
|
|
346
|
+
};
|
|
347
|
+
const pid = number('pid');
|
|
348
|
+
const at = number('at');
|
|
349
|
+
const given = (key) => {
|
|
350
|
+
const value = text(key);
|
|
351
|
+
return value === undefined ? {} : { [key]: value };
|
|
352
|
+
};
|
|
353
|
+
const agentName = text('name');
|
|
354
|
+
return {
|
|
355
|
+
name: 'presence',
|
|
356
|
+
origin,
|
|
357
|
+
...app(),
|
|
358
|
+
...(state ? { state } : {}),
|
|
359
|
+
...(session ? { session } : {}),
|
|
360
|
+
...given('cwd'),
|
|
361
|
+
...given('transcript'),
|
|
362
|
+
...(agentName ? { agentName } : {}),
|
|
363
|
+
...(flags.has('keep') ? { keep: true } : {}),
|
|
364
|
+
...(pid ? { pid } : {}),
|
|
365
|
+
...(at ? { at } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
304
368
|
case 'uninstall':
|
|
305
369
|
none();
|
|
306
370
|
return { name: 'uninstall', origin, ...apps() };
|
package/dist/claude.js
CHANGED
|
@@ -8,6 +8,7 @@ import { findHook, hooksOf, mergeHooks, originArgs, removeHooks } from "./hooks.
|
|
|
8
8
|
import { cliPackage, hookForm, hookRuns, shellQuote, } from "./install.js";
|
|
9
9
|
import { atLeast, claudeVersion, isOurModDir, loadsMods, modDir, PLUGIN_DIRS, pluginDirs, pluginEnabled, separatorFor, withModDir, } from "./mod.js";
|
|
10
10
|
import { removePermissionFiles } from "./permission.js";
|
|
11
|
+
import { removePresenceFiles } from "./presence.js";
|
|
11
12
|
import { helperPath } from "./remote.js";
|
|
12
13
|
import { readShim, SHIM } from "./shim.js";
|
|
13
14
|
import { VERSION } from "./version.js";
|
|
@@ -17,8 +18,9 @@ export const HOOKS = {
|
|
|
17
18
|
SessionStart: 'pickup',
|
|
18
19
|
UserPromptSubmit: 'handoff',
|
|
19
20
|
Stop: 'stopcheck',
|
|
21
|
+
SessionEnd: 'presence',
|
|
20
22
|
};
|
|
21
|
-
export const CLAUDE_HOOKS = Object.keys(HOOKS).map((event) => ({ event, sub: HOOKS[event] }));
|
|
23
|
+
export const CLAUDE_HOOKS = Object.keys(HOOKS).map((event) => ({ event, sub: HOOKS[event], ...(event === 'SessionEnd' ? { timeout: null } : {}) }));
|
|
22
24
|
export const PERMISSION_HOOKS = [
|
|
23
25
|
{ event: 'PermissionRequest', sub: 'permission' },
|
|
24
26
|
{ event: 'PostToolUse', sub: 'permission-done' },
|
|
@@ -339,7 +341,7 @@ export async function installClaude(io, origin, options) {
|
|
|
339
341
|
report({
|
|
340
342
|
ok: true,
|
|
341
343
|
text: merged.hooksChanged
|
|
342
|
-
? `Installed or updated the session-start pickup, next-message hand-off
|
|
344
|
+
? `Installed or updated the session-start pickup, next-message hand-off, Stop check and session-end hooks in ${settingsPath(io)}.`
|
|
343
345
|
: 'The hooks were already installed.',
|
|
344
346
|
});
|
|
345
347
|
const flag = originArgs(origin);
|
|
@@ -429,6 +431,7 @@ export async function uninstallClaude(io) {
|
|
|
429
431
|
if (modRemoved)
|
|
430
432
|
steps.push({ ok: true, text: `Removed the wake mod from ${PLUGIN_DIRS}.` });
|
|
431
433
|
await removePermissionFiles(io);
|
|
434
|
+
await removePresenceFiles(io, 'claude-code');
|
|
432
435
|
const skill = await removeSkill(manifest?.skill ?? null);
|
|
433
436
|
if (skill)
|
|
434
437
|
steps.push(skill);
|
|
@@ -493,6 +496,7 @@ export async function claudeState(io) {
|
|
|
493
496
|
hooks: Boolean(start && ours('UserPromptSubmit')),
|
|
494
497
|
stopcheck: Boolean(ours('Stop')),
|
|
495
498
|
permission,
|
|
499
|
+
presence: Boolean(ours('SessionEnd')),
|
|
496
500
|
form: form ? { ...form, runs: await hookRuns(form), shim } : null,
|
|
497
501
|
mod,
|
|
498
502
|
plugins,
|
package/dist/format.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
export const HANDED_PREFIX = 'Pending You: your person handed you';
|
|
2
|
+
export const HANDED_REPLY = 'on the question handed to you';
|
|
3
|
+
export const HANDED_TALK_TODO = 'If you know now, answer_delegated with your answer and how you know; if you need more, reply_in_thread to “asker”; if it’s your person’s to decide, reply_in_thread with escalate true; if not, hand_back with what you checked.';
|
|
4
|
+
export function isHandedWake(text) {
|
|
5
|
+
const first = text.trimStart().split('\n')[0] ?? '';
|
|
6
|
+
return (first.startsWith(HANDED_PREFIX) ||
|
|
7
|
+
(first.startsWith('Pending You: ') && first.includes(` ${HANDED_REPLY}, `)));
|
|
8
|
+
}
|
|
9
|
+
export function latestTalk(heard, from) {
|
|
10
|
+
const last = heard.messages?.at(-1);
|
|
11
|
+
if (!last || typeof last.body !== 'string' || !last.body.trim())
|
|
12
|
+
return null;
|
|
13
|
+
return { who: last.author === 'you' ? 'your person' : from, words: last.body };
|
|
14
|
+
}
|
|
2
15
|
export function delegatedOf(heard) {
|
|
3
16
|
const given = heard.delegated;
|
|
4
17
|
if (typeof given !== 'object' || given === null)
|
|
@@ -44,6 +57,22 @@ function describeHanded(heard, delegated) {
|
|
|
44
57
|
const id = heard.requestId;
|
|
45
58
|
const from = clip(delegated.from, MAX_NAME).text;
|
|
46
59
|
const note = delegated.note ? clip(delegated.note, MAX_MESSAGE) : null;
|
|
60
|
+
const talk = latestTalk(heard, from);
|
|
61
|
+
if (talk) {
|
|
62
|
+
let cut = false;
|
|
63
|
+
const wrote = heard.messages.map((message) => {
|
|
64
|
+
const said = quoted(message.body, MAX_MESSAGE);
|
|
65
|
+
cut ||= said.cut;
|
|
66
|
+
return `${message.author === 'you' ? 'Your person' : from}: ${said.line}`;
|
|
67
|
+
});
|
|
68
|
+
return [
|
|
69
|
+
`Pending You: ${talk.who === 'your person' ? 'your person wrote to you both' : `${from} replied`} ${HANDED_REPLY}, ${id} (${modeWords(delegated.mode, from)}).`,
|
|
70
|
+
`Question: ${clip(heard.title, MAX_TITLE).text}`,
|
|
71
|
+
...wrote,
|
|
72
|
+
`Call get_request (requestId ${id}, name ${named(heard)}) to read what you’ve said to each other. ${HANDED_TALK_TODO}`,
|
|
73
|
+
...(cut ? [`(Cut short: get_request ${id} has all of it.)`] : []),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
47
76
|
return [
|
|
48
77
|
`${HANDED_PREFIX} ${id}, a question from ${from} (${modeWords(delegated.mode, from)}).`,
|
|
49
78
|
`Question: ${clip(heard.title, MAX_TITLE).text}`,
|
package/dist/handed.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clip, HANDED_PREFIX, HANDED_TODO, modeWords, } from "./format.js";
|
|
1
|
+
import { clip, HANDED_PREFIX, HANDED_REPLY, HANDED_TALK_TODO, HANDED_TODO, latestTalk, modeWords, } from "./format.js";
|
|
2
2
|
export const HANDED_FOR_MS = 12 * 60 * 60_000;
|
|
3
3
|
export const HANDED_GRACE_MS = 5 * 60_000;
|
|
4
4
|
const TITLE_CHARS = 80;
|
|
@@ -42,30 +42,46 @@ export function namesAnotherComputer(machine, names) {
|
|
|
42
42
|
const ours = new Set(names.filter((name) => name.trim()).map(computerKey));
|
|
43
43
|
return !ours.has(computerKey(machine));
|
|
44
44
|
}
|
|
45
|
-
export const handedCard = (heard, delegated) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
export const handedCard = (heard, delegated) => {
|
|
46
|
+
const said = latestTalk(heard, delegated.from);
|
|
47
|
+
return {
|
|
48
|
+
requestId: heard.requestId,
|
|
49
|
+
title: heard.title,
|
|
50
|
+
from: delegated.from,
|
|
51
|
+
mode: delegated.mode,
|
|
52
|
+
note: Boolean(delegated.note),
|
|
53
|
+
name: heard.sessionLabel,
|
|
54
|
+
...(said ? { said } : {}),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const SAID_CHARS = 300;
|
|
58
|
+
const quotedWords = (words) => `“${clip(words.replace(/\s+/g, ' ').trim(), SAID_CHARS).text.replaceAll('“', '‘').replaceAll('”', '’')}”`;
|
|
59
|
+
const replied = (said) => said.who === 'your person'
|
|
60
|
+
? 'your person wrote to you both'
|
|
61
|
+
: `${clip(said.who, NAME_CHARS).text} replied`;
|
|
62
|
+
const readTalk = (card) => `get_request (requestId ${card.requestId}, name “${card.name}”) to read what you’ve said to each other`;
|
|
53
63
|
const titled = (card) => `“${clip(card.title, TITLE_CHARS).text.replaceAll('“', '‘').replaceAll('”', '’')}” (${card.requestId})`;
|
|
54
64
|
const asked = (card) => {
|
|
55
65
|
const from = clip(card.from, NAME_CHARS).text;
|
|
56
66
|
return `from ${from} (${modeWords(card.mode, from)})`;
|
|
57
67
|
};
|
|
58
68
|
const read = (card) => `get_request (requestId ${card.requestId}, name “${card.name}”) to read it${card.note ? ' and their note' : ''}`;
|
|
59
|
-
export function handedMessage(cards) {
|
|
69
|
+
export function handedMessage(cards, options = {}) {
|
|
60
70
|
if (cards.length === 0)
|
|
61
71
|
return null;
|
|
72
|
+
const words = options.words !== false;
|
|
73
|
+
const said = (card) => (card.said && words ? `: ${quotedWords(card.said.words)}` : '');
|
|
62
74
|
if (cards.length === 1) {
|
|
63
75
|
const card = cards[0];
|
|
76
|
+
if (card.said)
|
|
77
|
+
return `Pending You: ${replied(card.said)} ${HANDED_REPLY}, ${titled(card)}${said(card)}. Call ${readTalk(card)}. ${HANDED_TALK_TODO}`;
|
|
64
78
|
return `${HANDED_PREFIX} ${titled(card)}, a question ${asked(card)}. Call ${read(card)}. ${HANDED_TODO}`;
|
|
65
79
|
}
|
|
66
80
|
return [
|
|
67
81
|
`${HANDED_PREFIX} ${cards.length} questions from other assistants.`,
|
|
68
|
-
...cards.map((card) =>
|
|
69
|
-
|
|
82
|
+
...cards.map((card) => card.said
|
|
83
|
+
? `- ${titled(card)}, ${asked(card)}; ${replied(card.said)}${said(card)}: call ${readTalk(card)}.`
|
|
84
|
+
: `- ${titled(card)}, ${asked(card)}: call ${read(card)}.`),
|
|
85
|
+
`For each one: ${HANDED_TODO.charAt(0).toLowerCase()}${HANDED_TODO.slice(1)}${cards.some((card) => card.said) ? ` Where one replied, you may also reply_in_thread to “asker”, or escalate.` : ''}`,
|
|
70
86
|
].join('\n');
|
|
71
87
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DEFAULT_ORIGIN } from "./args.js";
|
|
|
2
2
|
import { PlainError } from "./errors.js";
|
|
3
3
|
import { shellQuote } from "./install.js";
|
|
4
4
|
export const HOOK_TIMEOUT = 10;
|
|
5
|
+
export const lineKey = (spec) => `${spec.event}:${spec.sub}`;
|
|
5
6
|
export const shWord = (text) => /^[\w@%+=:,./-]+$/.test(text) ? text : `'${text.replaceAll("'", `'\\''`)}'`;
|
|
6
7
|
export const originArgs = (origin) => origin === DEFAULT_ORIGIN ? '' : ` --origin ${shWord(origin)}`;
|
|
7
8
|
export function hookLine(shim, origin, app, sub) {
|
|
@@ -36,7 +37,7 @@ export function mergeHooks(hooks, specs, lines, where) {
|
|
|
36
37
|
const next = structuredClone(hooks);
|
|
37
38
|
let changed = false;
|
|
38
39
|
for (const spec of specs) {
|
|
39
|
-
const wanted = lines[spec.event];
|
|
40
|
+
const wanted = lines[lineKey(spec)] ?? lines[spec.event];
|
|
40
41
|
if (wanted === undefined)
|
|
41
42
|
continue;
|
|
42
43
|
if (next[spec.event] !== undefined && !Array.isArray(next[spec.event]))
|