pendingyou 0.14.0 → 0.16.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 +97 -0
- package/README.md +58 -15
- package/dist/api.js +27 -0
- package/dist/apps/claude-code.js +17 -12
- package/dist/apps/codex-wake.js +15 -3
- 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 +69 -1
- package/dist/claude.js +18 -4
- package/dist/format.js +65 -1
- package/dist/handed.js +28 -12
- package/dist/hooks.js +2 -1
- package/dist/io.js +1 -0
- package/dist/main.js +26 -4
- package/dist/permission.js +274 -72
- 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 +79 -3
- package/package.json +1 -1
package/dist/apps/codex.js
CHANGED
|
@@ -4,13 +4,15 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { removeSkill, saveSkill } from "../claude.js";
|
|
5
5
|
import { readCredential } from "../credentials.js";
|
|
6
6
|
import { configDir, readJson, readText, writeWhole } from "../files.js";
|
|
7
|
-
import { isOurHook, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
|
|
7
|
+
import { isOurHook, lineKey, mergeHooks, originArgs, removeHooks, shWord, } from "../hooks.js";
|
|
8
8
|
import { atLeast } from "../mod.js";
|
|
9
|
+
import { removePresenceFiles } from "../presence.js";
|
|
9
10
|
import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, } from "../remote.js";
|
|
10
11
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
11
12
|
import { basicString, definedInline, headerOf, renameTable, tableSpans, tableValues, withTable, } from "../toml.js";
|
|
12
13
|
import { findCodex } from "./codex-app.js";
|
|
13
14
|
import { APP_NAMES } from "./ids.js";
|
|
15
|
+
import { switches } from "./switch.js";
|
|
14
16
|
const NAME = APP_NAMES.codex;
|
|
15
17
|
const SERVER = ['mcp_servers', 'pendingyou'];
|
|
16
18
|
export const CODEX_MIN = '0.152.0';
|
|
@@ -27,11 +29,25 @@ export const CODEX_HOOKS = [
|
|
|
27
29
|
{ event: 'PostToolUse', sub: 'posted', matcher: CARD_TOOLS },
|
|
28
30
|
{ event: 'Stop', sub: 'stopcheck' },
|
|
29
31
|
];
|
|
32
|
+
export const CODEX_PERMISSION_HOOKS = [
|
|
33
|
+
{ event: 'PermissionRequest', sub: 'permission' },
|
|
34
|
+
{ event: 'PostToolUse', sub: 'permission-done' },
|
|
35
|
+
];
|
|
36
|
+
export const CODEX_PRESENCE_HOOKS = [
|
|
37
|
+
{ event: 'SessionEnd', sub: 'presence', timeout: 3 },
|
|
38
|
+
];
|
|
39
|
+
export const ALL_CODEX_HOOKS = [
|
|
40
|
+
...CODEX_HOOKS,
|
|
41
|
+
...CODEX_PERMISSION_HOOKS,
|
|
42
|
+
...CODEX_PRESENCE_HOOKS,
|
|
43
|
+
];
|
|
30
44
|
const EVENT_LABELS = {
|
|
31
45
|
SessionStart: 'session_start',
|
|
32
46
|
UserPromptSubmit: 'user_prompt_submit',
|
|
33
47
|
PostToolUse: 'post_tool_use',
|
|
34
48
|
Stop: 'stop',
|
|
49
|
+
PermissionRequest: 'permission_request',
|
|
50
|
+
SessionEnd: 'session_end',
|
|
35
51
|
};
|
|
36
52
|
const NO_MATCHER = new Set(['UserPromptSubmit', 'Stop']);
|
|
37
53
|
const SIGN_IN_KEYS = new Set([
|
|
@@ -151,7 +167,7 @@ async function hookTrust(io) {
|
|
|
151
167
|
const hooks = 'json' in file && file.json.hooks && typeof file.json.hooks === 'object'
|
|
152
168
|
? file.json.hooks
|
|
153
169
|
: {};
|
|
154
|
-
return
|
|
170
|
+
return ALL_CODEX_HOOKS.map((spec) => {
|
|
155
171
|
const groups = Array.isArray(hooks[spec.event]) ? hooks[spec.event] : [];
|
|
156
172
|
for (const [g, group] of groups.entries())
|
|
157
173
|
for (const [h, handler] of (group?.hooks ?? []).entries()) {
|
|
@@ -159,13 +175,14 @@ async function hookTrust(io) {
|
|
|
159
175
|
continue;
|
|
160
176
|
const state = tableValues(config, ['hooks', 'state', trustKey(file.path, spec.event, g, h)]);
|
|
161
177
|
if (state.enabled === false)
|
|
162
|
-
return { event: spec.event, trust: 'off' };
|
|
178
|
+
return { event: spec.event, sub: spec.sub, trust: 'off' };
|
|
163
179
|
const hash = hookHash(spec.event, group.matcher, {
|
|
164
180
|
command: handler.command,
|
|
165
181
|
...(typeof handler.timeout === 'number' ? { timeout: handler.timeout } : {}),
|
|
166
182
|
});
|
|
167
183
|
return {
|
|
168
184
|
event: spec.event,
|
|
185
|
+
sub: spec.sub,
|
|
169
186
|
trust: state.trusted_hash === hash
|
|
170
187
|
? 'trusted'
|
|
171
188
|
: typeof state.trusted_hash === 'string'
|
|
@@ -173,7 +190,7 @@ async function hookTrust(io) {
|
|
|
173
190
|
: 'untrusted',
|
|
174
191
|
};
|
|
175
192
|
}
|
|
176
|
-
return { event: spec.event, trust: 'missing' };
|
|
193
|
+
return { event: spec.event, sub: spec.sub, trust: 'missing' };
|
|
177
194
|
});
|
|
178
195
|
}
|
|
179
196
|
async function signedInByItself(io, program) {
|
|
@@ -191,15 +208,12 @@ async function signedInByItself(io, program) {
|
|
|
191
208
|
return null;
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const answer = await io.ask('Switch Codex to this computer’s sign-in? [Y/n] ');
|
|
201
|
-
return !/^\s*no?\s*$/i.test(answer);
|
|
202
|
-
}
|
|
211
|
+
const switchesFor = (io, ctx) => switches(io, ctx, {
|
|
212
|
+
name: 'Codex',
|
|
213
|
+
what: 'Codex signs in to Pending You by itself here (codex mcp login).',
|
|
214
|
+
uses: 'its hooks use',
|
|
215
|
+
headless: true,
|
|
216
|
+
});
|
|
203
217
|
async function writeConfig(path, text) {
|
|
204
218
|
return writeWhole(path, text).then(() => true, () => false);
|
|
205
219
|
}
|
|
@@ -210,24 +224,26 @@ function keepTheirTrust(config, file, before) {
|
|
|
210
224
|
'state',
|
|
211
225
|
trustKey(file, event, group, handler),
|
|
212
226
|
];
|
|
213
|
-
for (const
|
|
214
|
-
const
|
|
227
|
+
for (const event of [...new Set(ALL_CODEX_HOOKS.map((spec) => spec.event))]) {
|
|
228
|
+
const subs = ALL_CODEX_HOOKS.filter((spec) => spec.event === event).map((spec) => spec.sub);
|
|
229
|
+
const ours = (hook) => subs.some((sub) => isOurHook(hook?.command, sub));
|
|
230
|
+
const groups = Array.isArray(before[event]) ? before[event] : [];
|
|
215
231
|
for (const [g, group] of groups.entries())
|
|
216
232
|
for (const [h, hook] of (Array.isArray(group?.hooks) ? group.hooks : []).entries())
|
|
217
|
-
if (
|
|
218
|
-
next = withTable(next, state(
|
|
233
|
+
if (ours(hook))
|
|
234
|
+
next = withTable(next, state(event, g, h), null);
|
|
219
235
|
let at = 0;
|
|
220
236
|
for (const [g, group] of groups.entries()) {
|
|
221
237
|
if (!Array.isArray(group?.hooks)) {
|
|
222
238
|
at++;
|
|
223
239
|
continue;
|
|
224
240
|
}
|
|
225
|
-
const kept = group.hooks.flatMap((hook, h) => (
|
|
241
|
+
const kept = group.hooks.flatMap((hook, h) => (ours(hook) ? [] : [h]));
|
|
226
242
|
if (kept.length === 0)
|
|
227
243
|
continue;
|
|
228
244
|
for (const [h, was] of kept.entries())
|
|
229
245
|
if (g !== at || was !== h)
|
|
230
|
-
next = renameTable(next, state(
|
|
246
|
+
next = renameTable(next, state(event, g, was), state(event, at, h));
|
|
231
247
|
at++;
|
|
232
248
|
}
|
|
233
249
|
}
|
|
@@ -315,7 +331,7 @@ export const codex = {
|
|
|
315
331
|
else if ((await signedInByItself(io, program)) !== true) {
|
|
316
332
|
action = 'write';
|
|
317
333
|
}
|
|
318
|
-
else if (await
|
|
334
|
+
else if (await switchesFor(io, ctx))
|
|
319
335
|
action = 'write';
|
|
320
336
|
else {
|
|
321
337
|
mode = 'oauth';
|
|
@@ -327,6 +343,7 @@ export const codex = {
|
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
const version = detected.version;
|
|
346
|
+
let newHooks = null;
|
|
330
347
|
return {
|
|
331
348
|
app: codex,
|
|
332
349
|
detected,
|
|
@@ -384,7 +401,15 @@ export const codex = {
|
|
|
384
401
|
}
|
|
385
402
|
const skill = await saveSkill(io, ictx.origin, codexSkillPath(io), before?.skill ?? null);
|
|
386
403
|
report(skill.step);
|
|
387
|
-
const
|
|
404
|
+
const cards = ctx.permissionCards ?? before?.permissionCards !== false;
|
|
405
|
+
const extra = mode === 'helper' && ictx.helper !== null;
|
|
406
|
+
const wanted = [
|
|
407
|
+
...CODEX_HOOKS,
|
|
408
|
+
...(extra && cards ? CODEX_PERMISSION_HOOKS : []),
|
|
409
|
+
...(extra ? CODEX_PRESENCE_HOOKS : []),
|
|
410
|
+
];
|
|
411
|
+
const unwanted = ALL_CODEX_HOOKS.filter((spec) => !wanted.includes(spec));
|
|
412
|
+
const lines = Object.fromEntries(wanted.map((spec) => [lineKey(spec), ictx.hookLine(spec.sub)]));
|
|
388
413
|
const file = await readHooksFile(io);
|
|
389
414
|
if ('invalid' in file)
|
|
390
415
|
report({ ok: false, text: file.invalid });
|
|
@@ -393,14 +418,25 @@ export const codex = {
|
|
|
393
418
|
const hooks = file.json.hooks && typeof file.json.hooks === 'object'
|
|
394
419
|
? file.json.hooks
|
|
395
420
|
: {};
|
|
396
|
-
const merged = mergeHooks(hooks,
|
|
397
|
-
|
|
398
|
-
|
|
421
|
+
const merged = mergeHooks(hooks, wanted, lines, file.path);
|
|
422
|
+
const removed = removeHooks(merged.hooks, unwanted);
|
|
423
|
+
if (merged.changed || removed.changed)
|
|
424
|
+
await writeWhole(file.path, `${JSON.stringify({ ...file.json, hooks: removed.hooks }, null, 2)}\n`);
|
|
425
|
+
const fresh = wanted.filter((spec) => !CODEX_HOOKS.includes(spec) && !(lineKey(spec) in (before?.hooks ?? {})));
|
|
426
|
+
if (before && merged.changed && fresh.length)
|
|
427
|
+
newHooks = [
|
|
428
|
+
...(fresh.some((spec) => spec.sub === 'presence') ? ['session end'] : []),
|
|
429
|
+
...(fresh.some((spec) => spec.sub.startsWith('permission'))
|
|
430
|
+
? ['permission prompts']
|
|
431
|
+
: []),
|
|
432
|
+
].join(' and ');
|
|
399
433
|
report({
|
|
400
434
|
ok: true,
|
|
401
435
|
text: merged.changed
|
|
402
|
-
? `Installed or updated Codex’s session-start pickup, next-message hand-off, card and Stop check hooks in ${file.path}. Codex asks you to trust them once.`
|
|
403
|
-
:
|
|
436
|
+
? `Installed or updated Codex’s session-start pickup, next-message hand-off, card and Stop check hooks${extra ? `, ${cards ? 'its permission-prompt hooks (a card when Codex waits for your OK; --no-permission-cards leaves them out) ' : ''}and its session-end hook (Pending You hears when a thread is open)` : ''} in ${file.path}. Codex asks you to trust them once.`
|
|
437
|
+
: removed.changed
|
|
438
|
+
? `Took out Codex’s permission-prompt hooks, as you asked: no card when Codex waits for your OK. To turn them on: npx pendingyou init --app codex --permission-cards${originArgs(ictx.origin)}`
|
|
439
|
+
: 'Codex’s hooks were already installed.',
|
|
404
440
|
});
|
|
405
441
|
}
|
|
406
442
|
catch (error) {
|
|
@@ -426,6 +462,7 @@ export const codex = {
|
|
|
426
462
|
hooks: lines,
|
|
427
463
|
helper: mode === 'helper' ? ictx.helper : null,
|
|
428
464
|
setup: { since: new Date(io.now()).toISOString() },
|
|
465
|
+
permissionCards: cards,
|
|
429
466
|
};
|
|
430
467
|
await writeWhole(manifestPath(io), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
431
468
|
secret: true,
|
|
@@ -438,9 +475,19 @@ export const codex = {
|
|
|
438
475
|
const start = detected.command
|
|
439
476
|
? `open the Codex app, start a chat in the folder you work in, and trust Pending You’s hooks: click Review hooks › Allow all (or Settings › Hooks › Trust all)`
|
|
440
477
|
: `start codex here, or in the folder you work in, and trust Pending You’s hooks: ${TRUST_CLI}; ${TRUST_APP}`;
|
|
478
|
+
const again = newHooks
|
|
479
|
+
? `Codex has new Pending You hooks (${newHooks}), which it skips until you trust them once: ${detected.command ? TRUST_APP : `${TRUST_CLI}; ${TRUST_APP}`}.`
|
|
480
|
+
: null;
|
|
441
481
|
return {
|
|
442
|
-
lines: [
|
|
443
|
-
|
|
482
|
+
lines: [
|
|
483
|
+
`Next: ${first}${start}.`,
|
|
484
|
+
...(again ? [again] : []),
|
|
485
|
+
DONT_SAVE,
|
|
486
|
+
'Then say this to it:',
|
|
487
|
+
'',
|
|
488
|
+
FINISH_SAY,
|
|
489
|
+
],
|
|
490
|
+
together: `- Codex: ${first}${start}, then say the line below to it.${again ? ` ${again}` : ''} ${DONT_SAVE}`,
|
|
444
491
|
say: true,
|
|
445
492
|
};
|
|
446
493
|
},
|
|
@@ -454,7 +501,7 @@ export const codex = {
|
|
|
454
501
|
let doc = (await readText(config).catch(() => null)) ?? null;
|
|
455
502
|
if ('json' in file) {
|
|
456
503
|
const hooks = file.json.hooks && typeof file.json.hooks === 'object' ? file.json.hooks : {};
|
|
457
|
-
const removed = removeHooks(hooks,
|
|
504
|
+
const removed = removeHooks(hooks, ALL_CODEX_HOOKS);
|
|
458
505
|
if (removed.changed) {
|
|
459
506
|
const rest = { ...file.json, hooks: removed.hooks };
|
|
460
507
|
if (Object.keys(removed.hooks).length === 0)
|
|
@@ -484,6 +531,7 @@ export const codex = {
|
|
|
484
531
|
steps.push(skill);
|
|
485
532
|
await rm(join(configDir(io), 'codex-threads.json'), { force: true });
|
|
486
533
|
await rm(join(configDir(io), 'codex-listen'), { recursive: true, force: true });
|
|
534
|
+
await removePresenceFiles(io, 'codex');
|
|
487
535
|
await rm(manifestPath(io), { force: true });
|
|
488
536
|
return steps;
|
|
489
537
|
},
|
|
@@ -541,10 +589,30 @@ export const codex = {
|
|
|
541
589
|
off: 'turned off',
|
|
542
590
|
missing: 'missing',
|
|
543
591
|
};
|
|
544
|
-
const
|
|
545
|
-
const
|
|
592
|
+
const core = trust.filter((hook) => CODEX_HOOKS.some((spec) => spec.sub === hook.sub));
|
|
593
|
+
const hooksOk = core.every((hook) => hook.trust === 'trusted');
|
|
594
|
+
const hooksText = core.some((hook) => hook.trust === 'missing')
|
|
546
595
|
? `not installed; run npx pendingyou init --app codex${flag}`
|
|
547
|
-
: `${
|
|
596
|
+
: `${core.map((hook) => `${hook.event} ${words[hook.trust]}`).join(', ')}${hooksOk ? '' : `. Codex skips a hook you haven’t trusted: ${TRUST_CLI}; ${TRUST_APP}`}`;
|
|
597
|
+
const extraLine = (label, what, subs, off) => {
|
|
598
|
+
const hooks = trust.filter((hook) => subs.includes(hook.sub));
|
|
599
|
+
if (off)
|
|
600
|
+
return ` ${label} ${off}`;
|
|
601
|
+
if (hooks.some((hook) => hook.trust === 'missing'))
|
|
602
|
+
return ` missing ${label} ${what}: not installed; run npx pendingyou@latest init --app codex${flag}`;
|
|
603
|
+
const trusted = hooks.every((hook) => hook.trust === 'trusted');
|
|
604
|
+
return trusted
|
|
605
|
+
? ` ok ${label} ${what} (${hooks.map((hook) => `${hook.event} trusted`).join(', ')})`
|
|
606
|
+
: ` missing ${label} ${what}, once you trust ${hooks.length === 1 ? 'its hook' : 'its hooks'} (${hooks.map((hook) => `${hook.event} ${words[hook.trust]}`).join(', ')}): ${TRUST_CLI}; ${TRUST_APP}`;
|
|
607
|
+
};
|
|
608
|
+
const ownConnection = credential?.kind === 'connection' && helper;
|
|
609
|
+
const notOwn = 'off: it needs Codex signed in through this computer’s own connection';
|
|
610
|
+
const permissionText = extraLine('Permission prompts:', 'a card when Codex waits for your OK', CODEX_PERMISSION_HOOKS.map((spec) => spec.sub), !ownConnection
|
|
611
|
+
? notOwn
|
|
612
|
+
: manifest?.permissionCards === false
|
|
613
|
+
? `off, as you chose; for a card when Codex waits for your OK, run npx pendingyou init --app codex --permission-cards${flag}`
|
|
614
|
+
: null);
|
|
615
|
+
const presenceText = extraLine('Presence:', 'tells Pending You when this session is open', CODEX_PRESENCE_HOOKS.map((spec) => spec.sub), ownConnection ? null : notOwn);
|
|
548
616
|
const wakes = detected
|
|
549
617
|
? atLeast(detected.version, detected.command ? CODEX_APP_WAKE : CODEX_WAKE)
|
|
550
618
|
: false;
|
|
@@ -562,6 +630,8 @@ export const codex = {
|
|
|
562
630
|
: []),
|
|
563
631
|
` ${mark(skill !== null)} Skill: ${skill !== null ? `saved (${codexSkillPath(io)})` : `not saved; run npx pendingyou init --app codex${flag}`}`,
|
|
564
632
|
` ${mark(hooksOk)} Hooks: ${hooksText}`,
|
|
633
|
+
permissionText,
|
|
634
|
+
presenceText,
|
|
565
635
|
` ${mark(wakes)} Wake: ${wakes
|
|
566
636
|
? detected?.command
|
|
567
637
|
? 'codex queue wakes a chat the Codex app has open when you answer, while the app runs (one it let go, idle 3 hours or behind 10 newer idle chats, hears it when you open it again)'
|
|
@@ -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
|
+
}
|