pendingyou 0.22.0 → 0.23.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 +24 -0
- package/README.md +5 -0
- package/dist/apps/claude-code.js +27 -16
- package/dist/claude.js +26 -10
- package/dist/herdr/queue.js +5 -1
- package/dist/install.js +50 -4
- package/dist/loaded.js +88 -0
- package/dist/mod.js +8 -3
- package/dist/pickup.js +17 -6
- package/dist/presence.js +36 -7
- package/dist/state.js +15 -0
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +2 -2
- package/mod/hooks/register.js +14 -3
- package/mod/hooks/wake.js +19 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.23.0 (2026-10-06)
|
|
4
|
+
|
|
5
|
+
- The Claude Code wake mod survives upgrades. `init` now copies it to `~/.config/pendingyou/mod`, a folder that stays
|
|
6
|
+
put across versions, and points `CLAUDE_CODE_PLUGIN_DIRS` there, so the entry never changes again. Before, it named
|
|
7
|
+
the version's own folder, which the next upgrade removed: a session already open (Claude Code reads its plugin
|
|
8
|
+
folders only as a session starts) was left with no wake mod and nothing said, and answers reached it only when you
|
|
9
|
+
typed. Open interactive sessions reload the mod when `init` updates it. `init` keeps the newest older copy for
|
|
10
|
+
sessions started before this version, and `status` says when the entry still names a version.
|
|
11
|
+
- A session the wake mod doesn't run in is told so, once: with its next message, the hooks say “Pending You can’t wake
|
|
12
|
+
this session: Claude Code started before Pending You’s wake was set up. Ask your person to restart it (/exit, then
|
|
13
|
+
claude --continue) at a good moment.”, and to background a hold until then. The mod notes each session it runs in
|
|
14
|
+
(`~/.config/pendingyou/wake/sessions/`), and presence remembers when an older mod said a session is live.
|
|
15
|
+
- `init` names the Claude Code sessions open here that started before the wake mod was set up, by the name they go by
|
|
16
|
+
and their folder, and says to restart each at a good moment.
|
|
17
|
+
- A question handed to your agent by its name reaches it in whatever folder it works. The wake mod and the hooks took
|
|
18
|
+
one only in the folder Pending You heard it in (its task's, or the agent's last task's), so an agent working elsewhere
|
|
19
|
+
never heard it. The hooks now send the session's name, and the mod reads Pending You's `byName`; one handed to the app
|
|
20
|
+
with no agent still goes by its folder. Needs Pending You's matching release.
|
|
21
|
+
|
|
22
|
+
## 0.22.1 (2026-10-06)
|
|
23
|
+
|
|
24
|
+
- The Herdr plugin's popup finishes noting your last key press (what keeps your phone quiet while you're in Herdr)
|
|
25
|
+
before it closes, rather than leaving it half-written in the plugin's state folder.
|
|
26
|
+
|
|
3
27
|
## 0.22.0 (2026-10-06)
|
|
4
28
|
|
|
5
29
|
- Trusting Codex's hooks is one command now. When `init` or `status` finds Pending You's hooks in Codex but not trusted
|
package/README.md
CHANGED
|
@@ -354,6 +354,11 @@ with `claude --continue`. Under the prompt it shows `⚠ pendingyou-wake: 2 wait
|
|
|
354
354
|
`npx pendingyou hold`, the mod answers it at once and nothing runs in the background. With the Pending You plugin
|
|
355
355
|
installed as well, only one copy acts. `npx pendingyou status` says whether the mod is loaded.
|
|
356
356
|
|
|
357
|
+
Claude Code loads the mod only as a session starts, so a session that was already open when you ran `init` the first
|
|
358
|
+
time can't be woken until you restart it (`/exit`, then `claude --continue`): `init` names those sessions, and the
|
|
359
|
+
session itself is told once with your next message. Later upgrades don't need a restart: the mod lives in
|
|
360
|
+
`~/.config/pendingyou/mod`, which `init` updates in place, and open sessions reload it.
|
|
361
|
+
|
|
357
362
|
The mod's checks are Pending You tool calls like Claude Code's own, so they need Pending You's tools allowed, which
|
|
358
363
|
`init` does (`mcp__pendingyou`). If they aren't, the line under the prompt says which rule to add in `/permissions`, and
|
|
359
364
|
holds run as before.
|
package/dist/apps/claude-code.js
CHANGED
|
@@ -2,7 +2,8 @@ import { addCommand, claudeState, helperServer, installClaude, mcpFromClaude, mc
|
|
|
2
2
|
import { connectionMachine, readCredential } from "../credentials.js";
|
|
3
3
|
import { differs, serverForFolder, siteOf } from "../environment.js";
|
|
4
4
|
import { originArgs } from "../hooks.js";
|
|
5
|
-
import {
|
|
5
|
+
import { modConfigured, pruneLoadedNotes, unwokenLines, unwokenSessions, } from "../loaded.js";
|
|
6
|
+
import { claudeVersion, isOurModDir, loadsMods, MOD_CLAUDE } from "../mod.js";
|
|
6
7
|
import { checkHelper, foundBy, headlessReason, helperCommand, helperPath, isOurHelper, machineOf, usesHelper, } from "../remote.js";
|
|
7
8
|
import { connectionTitle, FINISH_SAY, setupOf, setupStatus } from "../setup.js";
|
|
8
9
|
import { shimPath } from "../shim.js";
|
|
@@ -144,32 +145,41 @@ export const claudeCode = {
|
|
|
144
145
|
plan = { action: 'add' };
|
|
145
146
|
const signIn = mode === 'helper' ? 'connection' : 'hear';
|
|
146
147
|
const added = plan.action !== 'keep';
|
|
148
|
+
let unwoken = [];
|
|
147
149
|
return {
|
|
148
150
|
app: claudeCode,
|
|
149
151
|
detected,
|
|
150
152
|
signIn,
|
|
151
|
-
install: (ictx) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
install: async (ictx) => {
|
|
154
|
+
const steps = await installClaude(io, ctx.origin, {
|
|
155
|
+
version: detected.version,
|
|
156
|
+
mcp: plan,
|
|
157
|
+
mode,
|
|
158
|
+
helper: ictx.helper,
|
|
159
|
+
plugin: Boolean(plugin),
|
|
160
|
+
copy: ictx.copy,
|
|
161
|
+
hookLine: ictx.hookLine,
|
|
162
|
+
report: ictx.report,
|
|
163
|
+
progress: ictx.progress,
|
|
164
|
+
...(ctx.permissionCards === undefined ? {} : { permissionCards: ctx.permissionCards }),
|
|
165
|
+
});
|
|
166
|
+
await pruneLoadedNotes(io).catch(() => { });
|
|
167
|
+
if (await modConfigured(io))
|
|
168
|
+
unwoken = await unwokenSessions(io).catch(() => []);
|
|
169
|
+
return steps;
|
|
170
|
+
},
|
|
163
171
|
next: (ictx) => {
|
|
172
|
+
const restart = unwokenLines(unwoken);
|
|
164
173
|
if (mode === 'oauth') {
|
|
165
174
|
const authenticate = added || !ictx.signedIn;
|
|
166
175
|
return {
|
|
167
176
|
lines: [
|
|
177
|
+
...restart,
|
|
168
178
|
authenticate
|
|
169
179
|
? 'Next, in Claude Code: if it’s open, restart it (/exit, then claude --continue), run /mcp, choose pendingyou and Authenticate. Then ask Claude Code to finish setting up Pending You.'
|
|
170
180
|
: 'Next, in Claude Code: if it’s open, restart it (/exit, then claude --continue), so the hooks load. Then ask Claude Code to finish setting up Pending You.',
|
|
171
181
|
],
|
|
172
|
-
together: `- Claude Code: restart it (/exit, then claude --continue)${authenticate ? ', run /mcp, choose pendingyou and Authenticate' : ''}, then ask it to finish setting up Pending You
|
|
182
|
+
together: `- Claude Code: restart it (/exit, then claude --continue)${authenticate ? ', run /mcp, choose pendingyou and Authenticate' : ''}, then ask it to finish setting up Pending You.${restart.length ? ` ${restart.join(' ')}` : ''}`,
|
|
173
183
|
say: false,
|
|
174
184
|
};
|
|
175
185
|
}
|
|
@@ -177,6 +187,7 @@ export const claudeCode = {
|
|
|
177
187
|
const byItself = version !== null && loadsMods(version);
|
|
178
188
|
return {
|
|
179
189
|
lines: [
|
|
190
|
+
...restart,
|
|
180
191
|
`Next: ${RESTART}.`,
|
|
181
192
|
byItself
|
|
182
193
|
? 'It finishes setting up by itself and sends you a test card. If it doesn’t, say this to it:'
|
|
@@ -184,7 +195,7 @@ export const claudeCode = {
|
|
|
184
195
|
'',
|
|
185
196
|
FINISH_SAY,
|
|
186
197
|
],
|
|
187
|
-
together: `- Claude Code: ${RESTART}. ${byItself ? 'It finishes setting up by itself and sends you a test card.' : 'Then say the line below to it.'}`,
|
|
198
|
+
together: `- Claude Code: ${RESTART}. ${byItself ? 'It finishes setting up by itself and sends you a test card.' : 'Then say the line below to it.'}${restart.length ? ` ${restart.join(' ')}` : ''}`,
|
|
188
199
|
say: true,
|
|
189
200
|
};
|
|
190
201
|
},
|
|
@@ -269,7 +280,7 @@ async function claudeStatus(io, ctx) {
|
|
|
269
280
|
: !mod.loads
|
|
270
281
|
? `needs Claude Code ${MOD_CLAUDE} or later (this is ${claude.claude}); until then, a background hold wakes it`
|
|
271
282
|
: mod.present
|
|
272
|
-
? `in CLAUDE_CODE_PLUGIN_DIRS (${mod.dirs.join(', ')}); a session is woken when you answer, with no hold running`
|
|
283
|
+
? `in CLAUDE_CODE_PLUGIN_DIRS (${mod.dirs.join(', ')}); a session is woken when you answer, with no hold running${mod.dirs.every((dir) => isOurModDir(dir)) ? `. Its folder names a version, so an update leaves open sessions without it: run npx pendingyou@latest init${flag}` : ''}`
|
|
273
284
|
: mod.plugin
|
|
274
285
|
? 'in the Pending You plugin; a session is woken when you answer, with no hold running'
|
|
275
286
|
: mod.dirs.length
|
package/dist/claude.js
CHANGED
|
@@ -5,8 +5,9 @@ import { DEFAULT_ORIGIN } from "./args.js";
|
|
|
5
5
|
import { PlainError } from "./errors.js";
|
|
6
6
|
import { claudeDir, configDir, readJson, readText, writeWhole } from "./files.js";
|
|
7
7
|
import { findHook, hooksOf, mergeHooks, originArgs, removeHooks } from "./hooks.js";
|
|
8
|
-
import { cliPackage, hookForm, hookRuns, shellQuote, } from "./install.js";
|
|
9
|
-
import {
|
|
8
|
+
import { cliPackage, hookForm, hookRuns, installMod, shellQuote, } from "./install.js";
|
|
9
|
+
import { removeLoadedNotes } from "./loaded.js";
|
|
10
|
+
import { atLeast, claudeVersion, isOurModDir, loadsMods, modDir, PLUGIN_DIRS, pluginDirs, pluginEnabled, separatorFor, stableModDir, withModDir, } from "./mod.js";
|
|
10
11
|
import { removePermissionFiles } from "./permission.js";
|
|
11
12
|
import { removePresenceFiles } from "./presence.js";
|
|
12
13
|
import { helperPath } from "./remote.js";
|
|
@@ -396,8 +397,8 @@ async function addMod(io, settings, installed, found) {
|
|
|
396
397
|
const version = claudeVersion(found);
|
|
397
398
|
if (!version || !loadsMods(version))
|
|
398
399
|
return unchanged;
|
|
399
|
-
const
|
|
400
|
-
if (!
|
|
400
|
+
const source = installed ? modDir(cliPackage(installed.prefix)) : null;
|
|
401
|
+
if (!source || !(await exists(join(source, 'hooks', 'hooks.json'))))
|
|
401
402
|
return {
|
|
402
403
|
...unchanged,
|
|
403
404
|
step: {
|
|
@@ -405,18 +406,31 @@ async function addMod(io, settings, installed, found) {
|
|
|
405
406
|
text: 'Couldn’t add the wake mod without the hooks’ own copy of pendingyou, so Claude Code hears answers through hold for now. Run init again to retry.',
|
|
406
407
|
},
|
|
407
408
|
};
|
|
408
|
-
const
|
|
409
|
+
const dir = stableModDir(io);
|
|
410
|
+
const separator = separatorFor(io.platform);
|
|
411
|
+
const added = withModDir(settings, dir, separator);
|
|
409
412
|
if ('invalid' in added)
|
|
410
413
|
return { ...unchanged, step: { ok: false, text: added.invalid } };
|
|
414
|
+
const copied = await installMod(source, dir).catch(() => null);
|
|
415
|
+
if (!copied)
|
|
416
|
+
return {
|
|
417
|
+
...unchanged,
|
|
418
|
+
step: { ok: false, text: `Couldn’t copy the wake mod to ${dir}. Run init again.` },
|
|
419
|
+
};
|
|
420
|
+
const moved = (pluginDirs(settings, separator) ?? []).some((entry) => isOurModDir(entry));
|
|
411
421
|
return {
|
|
412
422
|
settings: added.settings,
|
|
413
423
|
changed: added.changed,
|
|
414
424
|
dir,
|
|
415
425
|
step: {
|
|
416
426
|
ok: true,
|
|
417
|
-
text: added.changed
|
|
418
|
-
?
|
|
419
|
-
|
|
427
|
+
text: !added.changed
|
|
428
|
+
? copied.changed
|
|
429
|
+
? `Updated the wake mod in ${dir}: open Claude Code sessions reload it by themselves.`
|
|
430
|
+
: 'The wake mod was already in Claude Code’s settings.'
|
|
431
|
+
: moved
|
|
432
|
+
? `Moved the wake mod to ${dir}, a folder that stays put when pendingyou updates, so an update never leaves an open session without it.`
|
|
433
|
+
: `Added the wake mod to ${PLUGIN_DIRS} in ${settingsPath(io)}: Claude Code ${version} wakes a session when you answer, with no hold running.`,
|
|
420
434
|
},
|
|
421
435
|
};
|
|
422
436
|
}
|
|
@@ -425,7 +439,7 @@ export async function uninstallClaude(io) {
|
|
|
425
439
|
const steps = [];
|
|
426
440
|
const settings = await readSettings(io);
|
|
427
441
|
const removed = unmergeSettings(settings, manifest?.permissions ?? []);
|
|
428
|
-
const unmodded = withModDir(removed.settings, null, separatorFor(io.platform));
|
|
442
|
+
const unmodded = withModDir(removed.settings, null, separatorFor(io.platform), [stableModDir(io)]);
|
|
429
443
|
const modRemoved = !('invalid' in unmodded) && unmodded.changed;
|
|
430
444
|
if (removed.changed || modRemoved)
|
|
431
445
|
await writeSettings(io, 'invalid' in unmodded ? removed.settings : unmodded.settings);
|
|
@@ -437,6 +451,8 @@ export async function uninstallClaude(io) {
|
|
|
437
451
|
});
|
|
438
452
|
if (modRemoved)
|
|
439
453
|
steps.push({ ok: true, text: `Removed the wake mod from ${PLUGIN_DIRS}.` });
|
|
454
|
+
await rm(stableModDir(io), { recursive: true, force: true }).catch(() => { });
|
|
455
|
+
await removeLoadedNotes(io);
|
|
440
456
|
await removePermissionFiles(io);
|
|
441
457
|
await removePresenceFiles(io, 'claude-code');
|
|
442
458
|
const skill = await removeSkill(manifest?.skill ?? null);
|
|
@@ -484,7 +500,7 @@ export async function claudeState(io) {
|
|
|
484
500
|
let plugins = [];
|
|
485
501
|
try {
|
|
486
502
|
const settings = await readSettings(io);
|
|
487
|
-
mod.dirs = (pluginDirs(settings, separatorFor(io.platform)) ?? []).filter(isOurModDir);
|
|
503
|
+
mod.dirs = (pluginDirs(settings, separatorFor(io.platform)) ?? []).filter((entry) => isOurModDir(entry, [stableModDir(io)]));
|
|
488
504
|
mod.plugin = pluginEnabled(settings);
|
|
489
505
|
plugins = enabledPlugins(settings);
|
|
490
506
|
const all = objectAt(settings, 'hooks') ?? {};
|
package/dist/herdr/queue.js
CHANGED
|
@@ -201,6 +201,7 @@ export async function queuePopup(io, place, options) {
|
|
|
201
201
|
ended = true;
|
|
202
202
|
state = { ...state, note: failureWords(error) };
|
|
203
203
|
};
|
|
204
|
+
const telling = new Set();
|
|
204
205
|
const here = async () => {
|
|
205
206
|
await markActive(place, io.now());
|
|
206
207
|
if (!client || ended || !scopes.has('presence:desk'))
|
|
@@ -466,7 +467,9 @@ export async function queuePopup(io, place, options) {
|
|
|
466
467
|
if (next.done)
|
|
467
468
|
return 0;
|
|
468
469
|
pending = keys.next();
|
|
469
|
-
|
|
470
|
+
const told = here();
|
|
471
|
+
telling.add(told);
|
|
472
|
+
void told.finally(() => telling.delete(told)).catch(() => { });
|
|
470
473
|
for (const key of options.splitKeys(next.value)) {
|
|
471
474
|
const [after, effect] = queueKey(state, key, io.now());
|
|
472
475
|
state = after;
|
|
@@ -482,6 +485,7 @@ export async function queuePopup(io, place, options) {
|
|
|
482
485
|
stop();
|
|
483
486
|
await keys.return?.();
|
|
484
487
|
await feed.catch(() => { });
|
|
488
|
+
await Promise.allSettled(telling);
|
|
485
489
|
io.signal.removeEventListener('abort', stop);
|
|
486
490
|
io.out('\x1b[?25h');
|
|
487
491
|
}
|
package/dist/install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { cp, mkdir, readdir, realpath, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
-
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
4
|
-
import { configDir, readJson } from "./files.js";
|
|
2
|
+
import { cp, mkdir, readdir, readFile, realpath, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
4
|
+
import { configDir, readJson, readText, writeWhole } from "./files.js";
|
|
5
5
|
import { VERSION } from "./version.js";
|
|
6
6
|
export const cliRoot = (io) => join(configDir(io), 'cli');
|
|
7
7
|
export const cliPrefix = (io, version = VERSION) => join(cliRoot(io), version);
|
|
@@ -97,13 +97,59 @@ export async function installCli(io, progress) {
|
|
|
97
97
|
await rm(staging, { recursive: true, force: true }).catch(() => { });
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
function byVersion(a, b) {
|
|
101
|
+
const parts = (version) => version.split('.').map((part) => Number.parseInt(part, 10));
|
|
102
|
+
const [left, right] = [parts(a), parts(b)];
|
|
103
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
104
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
105
|
+
if (difference)
|
|
106
|
+
return difference;
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
const VERSION_NAME = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
100
111
|
export async function pruneCli(io, keep) {
|
|
101
112
|
const root = cliRoot(io);
|
|
102
113
|
const entries = await readdir(root).catch(() => []);
|
|
114
|
+
const previous = entries
|
|
115
|
+
.filter((entry) => entry !== keep && VERSION_NAME.test(entry))
|
|
116
|
+
.sort(byVersion)
|
|
117
|
+
.at(-1);
|
|
103
118
|
for (const entry of entries)
|
|
104
|
-
if (entry !== keep)
|
|
119
|
+
if (entry !== keep && entry !== previous)
|
|
105
120
|
await rm(join(root, entry), { recursive: true, force: true }).catch(() => { });
|
|
106
121
|
}
|
|
122
|
+
async function filesUnder(root, at = root) {
|
|
123
|
+
const found = [];
|
|
124
|
+
for (const entry of await readdir(at, { withFileTypes: true })) {
|
|
125
|
+
const path = join(at, entry.name);
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
if (at === root && entry.name === 'tests')
|
|
128
|
+
continue;
|
|
129
|
+
found.push(...(await filesUnder(root, path)));
|
|
130
|
+
}
|
|
131
|
+
else if (entry.isFile())
|
|
132
|
+
found.push(relative(root, path));
|
|
133
|
+
}
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
const MOD_ORDER = ['hooks/wake.js', 'hooks/register.js', 'hooks/hooks.json'];
|
|
137
|
+
const modRank = (path) => {
|
|
138
|
+
const at = MOD_ORDER.indexOf(path.replaceAll('\\', '/'));
|
|
139
|
+
return at === -1 ? MOD_ORDER.length : at;
|
|
140
|
+
};
|
|
141
|
+
export async function installMod(from, to) {
|
|
142
|
+
const files = (await filesUnder(from)).sort((a, b) => modRank(a) - modRank(b));
|
|
143
|
+
let changed = false;
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const text = await readFile(join(from, file), 'utf8');
|
|
146
|
+
if ((await readText(join(to, file)).catch(() => null)) === text)
|
|
147
|
+
continue;
|
|
148
|
+
await writeWhole(join(to, file), text, { mode: 0o644 });
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
151
|
+
return { changed };
|
|
152
|
+
}
|
|
107
153
|
export async function removeCli(io) {
|
|
108
154
|
const root = cliRoot(io);
|
|
109
155
|
if (!(await exists(root)))
|
package/dist/loaded.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { configDir, readJson } from "./files.js";
|
|
4
|
+
import { holdCommand } from "./hold.js";
|
|
5
|
+
import { readSaid } from "./presence.js";
|
|
6
|
+
import { claimUnwoken } from "./state.js";
|
|
7
|
+
const SESSION = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
|
|
8
|
+
export const LOADED_GRACE_MS = 15_000;
|
|
9
|
+
export const OPEN_FOR_MS = 12 * 60 * 60_000;
|
|
10
|
+
const KEEP_NOTES_MS = 7 * 24 * 60 * 60_000;
|
|
11
|
+
const notesDir = (io) => join(configDir(io), 'wake', 'sessions');
|
|
12
|
+
export const loadedPath = (io, session) => join(notesDir(io), `${session}.json`);
|
|
13
|
+
export function unwokenText(origin) {
|
|
14
|
+
return `Pending You can’t wake this session: Claude Code started before Pending You’s wake was set up. Ask your person to restart it (/exit, then claude --continue) at a good moment. Until then, after each card, run \`${holdCommand('<requestId>', origin)}\` in the background.`;
|
|
15
|
+
}
|
|
16
|
+
export async function modConfigured(io) {
|
|
17
|
+
const manifest = await readJson(join(configDir(io), 'claude-code.json')).catch(() => null);
|
|
18
|
+
return typeof manifest?.modDir === 'string' && manifest.modDir !== '';
|
|
19
|
+
}
|
|
20
|
+
export async function modLoaded(io, session, said) {
|
|
21
|
+
if (said?.mod)
|
|
22
|
+
return true;
|
|
23
|
+
if (!SESSION.test(session))
|
|
24
|
+
return false;
|
|
25
|
+
return stat(loadedPath(io, session)).then(() => true, () => false);
|
|
26
|
+
}
|
|
27
|
+
export async function unwokenLine(io, origin, session, said) {
|
|
28
|
+
try {
|
|
29
|
+
if (!session || !SESSION.test(session))
|
|
30
|
+
return null;
|
|
31
|
+
if (!(await modConfigured(io)))
|
|
32
|
+
return null;
|
|
33
|
+
const latest = (await readSaid(io, 'claude-code').catch(() => ({})))[session];
|
|
34
|
+
if ((await modLoaded(io, session, said)) || latest?.mod)
|
|
35
|
+
return null;
|
|
36
|
+
if (said?.since !== undefined && io.now() - said.since < LOADED_GRACE_MS)
|
|
37
|
+
return null;
|
|
38
|
+
if (!(await claimUnwoken(io, session)))
|
|
39
|
+
return null;
|
|
40
|
+
return unwokenText(origin);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function unwokenSessions(io) {
|
|
47
|
+
const said = await readSaid(io, 'claude-code').catch(() => ({}));
|
|
48
|
+
const found = [];
|
|
49
|
+
for (const [session, each] of Object.entries(said)) {
|
|
50
|
+
if (each.state !== 'live' || io.now() - each.at > OPEN_FOR_MS)
|
|
51
|
+
continue;
|
|
52
|
+
if (await modLoaded(io, session, each))
|
|
53
|
+
continue;
|
|
54
|
+
found.push({
|
|
55
|
+
session,
|
|
56
|
+
at: each.at,
|
|
57
|
+
...(each.cwd ? { cwd: each.cwd } : {}),
|
|
58
|
+
...(each.name ? { name: each.name } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return found.sort((a, b) => b.at - a.at).map(({ at: _, ...rest }) => rest);
|
|
62
|
+
}
|
|
63
|
+
const named = (each) => `${each.name ? `“${each.name}”` : 'a session'}${each.cwd ? ` in ${each.cwd}` : ''}`;
|
|
64
|
+
const NAMED = 4;
|
|
65
|
+
export function unwokenLines(sessions) {
|
|
66
|
+
if (sessions.length === 0)
|
|
67
|
+
return [];
|
|
68
|
+
const shown = sessions.slice(0, NAMED).map(named);
|
|
69
|
+
const more = sessions.length - shown.length;
|
|
70
|
+
const list = `${shown.join('; ')}${more ? `; and ${more} more` : ''}`;
|
|
71
|
+
return [
|
|
72
|
+
sessions.length === 1
|
|
73
|
+
? `Pending You can’t wake the Claude Code session open here (${list}): it started before Pending You’s wake was set up. Restart it at a good moment: /exit, then claude --continue.`
|
|
74
|
+
: `Pending You can’t wake ${sessions.length} Claude Code sessions open here (${list}): they started before Pending You’s wake was set up. Restart each at a good moment: /exit, then claude --continue.`,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
export async function pruneLoadedNotes(io) {
|
|
78
|
+
const dir = notesDir(io);
|
|
79
|
+
for (const name of await readdir(dir).catch(() => [])) {
|
|
80
|
+
const path = join(dir, name);
|
|
81
|
+
const info = await stat(path).catch(() => null);
|
|
82
|
+
if (info && io.now() - info.mtimeMs > KEEP_NOTES_MS)
|
|
83
|
+
await rm(path, { force: true }).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function removeLoadedNotes(io) {
|
|
87
|
+
await rm(join(configDir(io), 'wake'), { recursive: true, force: true }).catch(() => { });
|
|
88
|
+
}
|
package/dist/mod.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { configDir } from "./files.js";
|
|
2
3
|
export const MOD_CLAUDE = '2.1.287';
|
|
3
4
|
export const PLUGIN_DIRS = 'CLAUDE_CODE_PLUGIN_DIRS';
|
|
4
5
|
export const PLUGINS_WITH_MOD = ['pending-you@pendingyou', 'pending-you-staging@pendingyou-staging'];
|
|
5
6
|
export const modDir = (packageRoot) => join(packageRoot, 'mod');
|
|
7
|
+
export const stableModDir = (io) => join(configDir(io), 'mod');
|
|
6
8
|
export function claudeVersion(output) {
|
|
7
9
|
return /^\s*(\d+\.\d+\.\d+)\b/.exec(output)?.[1] ?? null;
|
|
8
10
|
}
|
|
@@ -18,7 +20,9 @@ export function atLeast(version, least) {
|
|
|
18
20
|
return true;
|
|
19
21
|
}
|
|
20
22
|
export const loadsMods = (version) => atLeast(version, MOD_CLAUDE);
|
|
21
|
-
|
|
23
|
+
const bare = (path) => path.trim().replace(/[\\/]+$/, '');
|
|
24
|
+
export const isOurModDir = (entry, also = []) => /(^|[\\/])node_modules[\\/]pendingyou[\\/]mod[\\/]?$/.test(entry.trim()) ||
|
|
25
|
+
also.some((dir) => bare(dir) === bare(entry));
|
|
22
26
|
export const separatorFor = (platform) => (platform === 'win32' ? ';' : ':');
|
|
23
27
|
const objectAt = (parent, key) => {
|
|
24
28
|
const value = parent[key];
|
|
@@ -37,7 +41,7 @@ export function pluginDirs(settings, separator) {
|
|
|
37
41
|
return null;
|
|
38
42
|
return value.split(separator).filter((entry) => entry.trim() !== '');
|
|
39
43
|
}
|
|
40
|
-
export function withModDir(settings, dir, separator) {
|
|
44
|
+
export function withModDir(settings, dir, separator, also = []) {
|
|
41
45
|
const entries = pluginDirs(settings, separator);
|
|
42
46
|
if (entries === null)
|
|
43
47
|
return {
|
|
@@ -45,7 +49,8 @@ export function withModDir(settings, dir, separator) {
|
|
|
45
49
|
};
|
|
46
50
|
if (dir?.includes(separator))
|
|
47
51
|
return { invalid: `The wake mod’s folder has a “${separator}” in its path: ${dir}` };
|
|
48
|
-
const
|
|
52
|
+
const ours = dir ? [...also, dir] : also;
|
|
53
|
+
const wanted = [...entries.filter((entry) => !isOurModDir(entry, ours)), ...(dir ? [dir] : [])];
|
|
49
54
|
if (wanted.join(separator) === entries.join(separator))
|
|
50
55
|
return { settings, changed: false };
|
|
51
56
|
const next = structuredClone(settings);
|
package/dist/pickup.js
CHANGED
|
@@ -8,8 +8,9 @@ import { clip, delegatedOf, describe, HANDED_PREFIX, isHandedWake, } from "./for
|
|
|
8
8
|
import { goesBy, HANDED_GRACE_MS, namesAnotherComputer } from "./handed.js";
|
|
9
9
|
import { herdrTarget, reportCommand } from "./herdr.js";
|
|
10
10
|
import { holdCommand } from "./hold.js";
|
|
11
|
+
import { unwokenLine } from "./loaded.js";
|
|
11
12
|
import { isPermissionCard, isPromptApp, sessionName, settlePrompts } from "./permission.js";
|
|
12
|
-
import { announce, keepPresent } from "./presence.js";
|
|
13
|
+
import { announce, keepPresent, readSaid } from "./presence.js";
|
|
13
14
|
import { setupReminder } from "./setup.js";
|
|
14
15
|
import { claimHanded, markHanded, momentOf, releaseHanded, wasHanded } from "./state.js";
|
|
15
16
|
const TIME_BOX_MS = { pickup: 2500, handoff: 2000 };
|
|
@@ -115,11 +116,15 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
115
116
|
let line = null;
|
|
116
117
|
let warning = null;
|
|
117
118
|
let reminder = null;
|
|
119
|
+
let unwoken = null;
|
|
118
120
|
try {
|
|
119
121
|
const input = await hookInput(io);
|
|
120
122
|
const { cwd } = input;
|
|
121
123
|
if (mode === 'handoff' && isPromptApp(app))
|
|
122
124
|
await settlePrompts(io, { session: input.session, event: 'prompt' });
|
|
125
|
+
const known = mode === 'handoff' && app === DEFAULT_APP && input.session
|
|
126
|
+
? (await readSaid(io, app).catch(() => ({})))[input.session]
|
|
127
|
+
: undefined;
|
|
123
128
|
if (app === 'codex')
|
|
124
129
|
await keepPresent(io, origin, input.session, input.given ? cwd : null);
|
|
125
130
|
else if (app === DEFAULT_APP && (mode === 'pickup' || input.session))
|
|
@@ -144,6 +149,8 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
144
149
|
return 0;
|
|
145
150
|
line = REMINDER;
|
|
146
151
|
hook.fallback = `${[REMINDER, warning].filter(Boolean).join('\n')}\n`;
|
|
152
|
+
if (app === DEFAULT_APP)
|
|
153
|
+
unwoken = await unwokenLine(io, origin, input.session, known);
|
|
147
154
|
}
|
|
148
155
|
if (app !== DEFAULT_APP && input.session) {
|
|
149
156
|
await touchThread(io, app, input.session, input.given ? input.cwd : null);
|
|
@@ -152,11 +159,16 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
152
159
|
}
|
|
153
160
|
if (app === 'codex' && input.session)
|
|
154
161
|
await keepListening(io, origin, input.session);
|
|
162
|
+
const claudeNames = app === DEFAULT_APP
|
|
163
|
+
? sessionName(input.transcript ?? undefined).then((name) => (name ? [name] : []))
|
|
164
|
+
: null;
|
|
165
|
+
const named = claudeNames ? (await claudeNames)[0] : undefined;
|
|
155
166
|
const query = new URLSearchParams([
|
|
156
167
|
...folderForms(cwd, io.home).map((form) => ['cwd', form]),
|
|
157
168
|
['limit', String(ANSWERS_ASKED)],
|
|
158
169
|
...(mode === 'handoff' ? [['open', String(OPEN_SHOWN)]] : []),
|
|
159
170
|
['source', app],
|
|
171
|
+
...(named ? [['name', named]] : []),
|
|
160
172
|
]);
|
|
161
173
|
const { status, body } = await getJson(io, origin, `/mcp/cli/answers?${query}`, TIME_BOX_MS[mode], io.signal, {
|
|
162
174
|
hook: true,
|
|
@@ -190,10 +202,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
190
202
|
return computers !== null && namesAnotherComputer(heard.machine, computers);
|
|
191
203
|
},
|
|
192
204
|
names: () => {
|
|
193
|
-
names ??=
|
|
194
|
-
app === DEFAULT_APP
|
|
195
|
-
? sessionName(input.transcript ?? undefined).then((name) => (name ? [name] : []))
|
|
196
|
-
: threadNames(io, app, input.session);
|
|
205
|
+
names ??= claudeNames ?? threadNames(io, app, input.session);
|
|
197
206
|
return names;
|
|
198
207
|
},
|
|
199
208
|
};
|
|
@@ -241,10 +250,12 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
241
250
|
...(line ? [line] : []),
|
|
242
251
|
...(warning ? [warning] : []),
|
|
243
252
|
...(reminder ? [reminder] : []),
|
|
253
|
+
...(unwoken ? [unwoken] : []),
|
|
244
254
|
].join('\n')}\n`);
|
|
245
255
|
line = null;
|
|
246
256
|
warning = null;
|
|
247
257
|
reminder = null;
|
|
258
|
+
unwoken = null;
|
|
248
259
|
await markHanded(io, origin, fresh.map((heard) => ({ requestId: heard.requestId, moment: momentOf(heard) })));
|
|
249
260
|
return 0;
|
|
250
261
|
}
|
|
@@ -256,7 +267,7 @@ export async function pickup(io, options, hook = { fallback: '' }) {
|
|
|
256
267
|
return 0;
|
|
257
268
|
}
|
|
258
269
|
finally {
|
|
259
|
-
const last = [line, warning, reminder].filter(Boolean);
|
|
270
|
+
const last = [line, warning, reminder, unwoken].filter(Boolean);
|
|
260
271
|
if (last.length)
|
|
261
272
|
io.out(`${last.join('\n')}\n`);
|
|
262
273
|
}
|
package/dist/presence.js
CHANGED
|
@@ -100,15 +100,28 @@ export async function sendPresence(io, origin, presence, timeoutMs = SEND_MS, op
|
|
|
100
100
|
}
|
|
101
101
|
const recordPath = (io, app) => join(configDir(io), 'presence', `${app}.json`);
|
|
102
102
|
const leasePath = (io, app, session) => join(configDir(io), 'presence', `${app}-${session}.lease`);
|
|
103
|
-
|
|
103
|
+
const numberOr = (value) => (typeof value === 'number' ? value : undefined);
|
|
104
|
+
export async function readSaid(io, app) {
|
|
104
105
|
const file = await readJson(recordPath(io, app)).catch(() => null);
|
|
105
106
|
const sessions = {};
|
|
106
107
|
if (isObject(file?.sessions))
|
|
107
108
|
for (const [id, value] of Object.entries(file.sessions))
|
|
108
109
|
if (isObject(value) &&
|
|
109
110
|
(value.state === 'live' || value.state === 'closed') &&
|
|
110
|
-
typeof value.at === 'number')
|
|
111
|
-
|
|
111
|
+
typeof value.at === 'number') {
|
|
112
|
+
const since = numberOr(value.since);
|
|
113
|
+
const mod = numberOr(value.mod);
|
|
114
|
+
const cwd = text(value.cwd);
|
|
115
|
+
const name = text(value.name);
|
|
116
|
+
sessions[id] = {
|
|
117
|
+
state: value.state,
|
|
118
|
+
at: value.at,
|
|
119
|
+
...(since !== undefined ? { since } : {}),
|
|
120
|
+
...(mod !== undefined ? { mod } : {}),
|
|
121
|
+
...(cwd ? { cwd } : {}),
|
|
122
|
+
...(name ? { name } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
112
125
|
return sessions;
|
|
113
126
|
}
|
|
114
127
|
async function noteSaid(io, app, session, said) {
|
|
@@ -118,7 +131,14 @@ async function noteSaid(io, app, session, said) {
|
|
|
118
131
|
const before = sessions[session] ?? null;
|
|
119
132
|
if (before && before.at > said.at)
|
|
120
133
|
return { newer: true, before };
|
|
121
|
-
sessions[session] =
|
|
134
|
+
sessions[session] = {
|
|
135
|
+
...before,
|
|
136
|
+
...said,
|
|
137
|
+
since: before ? Math.min(before.since ?? 0, said.at) : said.at,
|
|
138
|
+
...(said.mod !== undefined || before?.mod !== undefined
|
|
139
|
+
? { mod: Math.max(said.mod ?? 0, before?.mod ?? 0) }
|
|
140
|
+
: {}),
|
|
141
|
+
};
|
|
122
142
|
const now = io.now();
|
|
123
143
|
for (const [id, each] of Object.entries(sessions))
|
|
124
144
|
if (now - each.at > FORGET_MS)
|
|
@@ -131,7 +151,16 @@ async function noteSaid(io, app, session, said) {
|
|
|
131
151
|
}
|
|
132
152
|
async function sendOnce(io, command) {
|
|
133
153
|
const at = command.at ?? io.now();
|
|
134
|
-
const
|
|
154
|
+
const cwd = presenceCwd(command.cwd, io.home);
|
|
155
|
+
const name = await nameFor(io, command);
|
|
156
|
+
const mod = command.app === 'claude-code' && command.state === 'live' && !command.transcript;
|
|
157
|
+
const noted = await noteSaid(io, command.app, command.session, {
|
|
158
|
+
state: command.state,
|
|
159
|
+
at,
|
|
160
|
+
...(mod ? { mod: at } : {}),
|
|
161
|
+
...(cwd ? { cwd } : {}),
|
|
162
|
+
...(name ? { name } : {}),
|
|
163
|
+
});
|
|
135
164
|
if (noted.newer)
|
|
136
165
|
return null;
|
|
137
166
|
if (command.state === 'live' &&
|
|
@@ -142,8 +171,8 @@ async function sendOnce(io, command) {
|
|
|
142
171
|
app: command.app,
|
|
143
172
|
session: command.session,
|
|
144
173
|
state: command.state,
|
|
145
|
-
cwd
|
|
146
|
-
name
|
|
174
|
+
cwd,
|
|
175
|
+
name,
|
|
147
176
|
}, SEND_MS, { patient: true });
|
|
148
177
|
}
|
|
149
178
|
const GIVE_UP = new Set(['refused', 'absent', 'skipped']);
|
package/dist/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { configDir, readJson, withLock, writeWhole } from "./files.js";
|
|
3
3
|
const KEEP_HANDED = 300;
|
|
4
|
+
const KEEP_UNWOKEN = 100;
|
|
4
5
|
const statePath = (io) => join(configDir(io), 'state.json');
|
|
5
6
|
const key = (origin, requestId) => `${origin} ${requestId}`;
|
|
6
7
|
export const momentOf = (heard) => `${heard.version}:${heard.messages.at(-1)?.id ?? ''}`;
|
|
@@ -10,6 +11,7 @@ async function load(io) {
|
|
|
10
11
|
version: 1,
|
|
11
12
|
handed: file?.handed && typeof file.handed === 'object' ? file.handed : {},
|
|
12
13
|
...(file?.reminded && typeof file.reminded === 'object' ? { reminded: file.reminded } : {}),
|
|
14
|
+
...(file?.unwoken && typeof file.unwoken === 'object' ? { unwoken: file.unwoken } : {}),
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
async function change(io, edit) {
|
|
@@ -20,6 +22,9 @@ async function change(io, edit) {
|
|
|
20
22
|
const handed = Object.entries(state.handed);
|
|
21
23
|
if (handed.length > KEEP_HANDED)
|
|
22
24
|
state.handed = Object.fromEntries(handed.slice(-KEEP_HANDED));
|
|
25
|
+
const unwoken = Object.entries(state.unwoken ?? {});
|
|
26
|
+
if (unwoken.length > KEEP_UNWOKEN)
|
|
27
|
+
state.unwoken = Object.fromEntries(unwoken.slice(-KEEP_UNWOKEN));
|
|
23
28
|
await writeWhole(path, `${JSON.stringify(state, null, 2)}\n`, { secret: true });
|
|
24
29
|
});
|
|
25
30
|
}
|
|
@@ -70,3 +75,13 @@ export function markReminded(io, origin, app, since) {
|
|
|
70
75
|
state.reminded = { ...state.reminded, [name]: { since, times, at: io.now() } };
|
|
71
76
|
});
|
|
72
77
|
}
|
|
78
|
+
export async function claimUnwoken(io, session) {
|
|
79
|
+
let claimed = false;
|
|
80
|
+
await change(io, (state) => {
|
|
81
|
+
if (state.unwoken?.[session] !== undefined)
|
|
82
|
+
return;
|
|
83
|
+
state.unwoken = { ...state.unwoken, [session]: io.now() };
|
|
84
|
+
claimed = true;
|
|
85
|
+
});
|
|
86
|
+
return claimed;
|
|
87
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.23.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.34.
|
|
4
|
+
"version": "2.34.2",
|
|
5
5
|
"description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "RecordPlane",
|
package/mod/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Pending You wake mod
|
|
2
2
|
|
|
3
|
-
The pendingyou command line’s copy of the Pending You plugin’s wake mod, for Claude Code 2.1.287 and later. `npx pendingyou init` adds
|
|
3
|
+
The pendingyou command line’s copy of the Pending You plugin’s wake mod, for Claude Code 2.1.287 and later. `npx pendingyou init` copies it to `~/.config/pendingyou/mod`, a folder that stays put when pendingyou updates, and adds that folder to `CLAUDE_CODE_PLUGIN_DIRS` in `~/.claude/settings.json`, so every Claude Code session started after loads it (one already open needs a restart: /exit, then claude --continue); `npx pendingyou uninstall` takes it out.
|
|
4
4
|
|
|
5
5
|
In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
|
|
6
6
|
|
|
7
|
-
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.34.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.34.2; send changes there.
|
package/mod/hooks/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generated from packages/claude-plugin/src/wake/register.ts in recordplane/pendingyou: edit that, then run
|
|
2
2
|
// `pnpm --filter @pendingyou/claude-plugin generate`.
|
|
3
3
|
|
|
4
|
-
import { argumentsOf, blockedNote, CLAIM_PREFIX, CLI_COPY, cardTool, checked, claimLease, cliConfigOf, connectionOf, dueCards, emptyBook, failed, folderOf, HANDED_FAST_MS, HANDED_READS, HERDR_EVERY_MS, HERDR_RUN_MS, handedDue, handedNext, handedOf, handedText, handedView, herdrArgv, herdrPayload, holdOf, holdReply, inHerdr, isRefusal, isStale, isWaitingOnYou, KEEP_MS, learn, leaseHolder, momentOf, originOf, PRESENCE_EVERY_MS, PRESENCE_RUN_MS, presenceArgv, prompted, put, readBook, readClaim, readSetup, readSince, resultObject, SETUP_AFTER_MS, SETUP_RETRY_MS, SETUP_SERVER, SETUP_TRIES, settle, setUpOf, setupNeeded, setupPrompt, stateOf, statusText, TICK_MS, takesHanded, told, toTell, untold, wakeText, } from "./wake.js";
|
|
4
|
+
import { argumentsOf, blockedNote, CLAIM_PREFIX, CLI_COPY, cardTool, checked, claimLease, cliConfigOf, connectionOf, dueCards, emptyBook, failed, folderOf, HANDED_FAST_MS, HANDED_READS, HERDR_EVERY_MS, HERDR_RUN_MS, handedDue, handedNext, handedOf, handedText, handedView, herdrArgv, herdrPayload, holdOf, holdReply, inHerdr, isRefusal, isStale, isWaitingOnYou, KEEP_MS, learn, leaseHolder, loadedNote, loadedPath, momentOf, originOf, PRESENCE_EVERY_MS, PRESENCE_RUN_MS, presenceArgv, prompted, put, readBook, readClaim, readSetup, readSince, resultObject, SETUP_AFTER_MS, SETUP_RETRY_MS, SETUP_SERVER, SETUP_TRIES, settle, setUpOf, setupNeeded, setupPrompt, stateOf, statusText, TICK_MS, takesHanded, told, toTell, untold, wakeText, } from "./wake.js";
|
|
5
5
|
const RETRY_MS = 60_000;
|
|
6
6
|
const live = {
|
|
7
7
|
me: '',
|
|
@@ -85,7 +85,7 @@ async function startHerdr($) {
|
|
|
85
85
|
live.herdrTimer = null;
|
|
86
86
|
live.herdr = null;
|
|
87
87
|
live.herdrSaid = undefined;
|
|
88
|
-
const config = cliConfigOf($.plugin.root);
|
|
88
|
+
const config = cliConfigOf($.plugin.root, $.plugin.name);
|
|
89
89
|
if (!config)
|
|
90
90
|
return;
|
|
91
91
|
const herdr = inHerdr(await $.env.get('HERDR_ENV'), await $.env.get('HERDR_BIN_PATH'), await $.env.get('HERDR_PANE_ID'));
|
|
@@ -319,6 +319,15 @@ async function folderOfSession($, cwd) {
|
|
|
319
319
|
live.home = undefined;
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
+
async function noteLoaded($) {
|
|
323
|
+
try {
|
|
324
|
+
const config = cliConfigOf($.plugin.root, $.plugin.name);
|
|
325
|
+
const path = config && live.sessionId ? loadedPath(config, live.sessionId) : null;
|
|
326
|
+
if (path)
|
|
327
|
+
await $.fs.write(path, loadedNote(await $.clock.now()));
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
}
|
|
322
331
|
async function sayLive($) {
|
|
323
332
|
try {
|
|
324
333
|
const presence = live.presence;
|
|
@@ -338,7 +347,7 @@ async function startPresence($) {
|
|
|
338
347
|
live.presenceTimer?.cancel();
|
|
339
348
|
live.presenceTimer = null;
|
|
340
349
|
live.presence = null;
|
|
341
|
-
const config = cliConfigOf($.plugin.root);
|
|
350
|
+
const config = cliConfigOf($.plugin.root, $.plugin.name);
|
|
342
351
|
if (!config)
|
|
343
352
|
return;
|
|
344
353
|
let manifest = null;
|
|
@@ -359,6 +368,7 @@ async function start($, e) {
|
|
|
359
368
|
live.book = readBook(await $.store.get(keyOf(live.sessionId)));
|
|
360
369
|
freshHanded();
|
|
361
370
|
await folderOfSession($, e.cwd);
|
|
371
|
+
await noteLoaded($);
|
|
362
372
|
await claim($);
|
|
363
373
|
live.timer?.cancel();
|
|
364
374
|
live.timer = $.clock.every(TICK_MS, () => tick($));
|
|
@@ -377,6 +387,7 @@ async function switchSession($, e) {
|
|
|
377
387
|
live.book = carried ?? readBook(await $.store.get(keyOf(id)));
|
|
378
388
|
live.noted.clear();
|
|
379
389
|
freshHanded();
|
|
390
|
+
await noteLoaded($);
|
|
380
391
|
if (typeof e.cwd === 'string' && e.cwd)
|
|
381
392
|
await folderOfSession($, e.cwd);
|
|
382
393
|
if (carried)
|
package/mod/hooks/wake.js
CHANGED
|
@@ -616,7 +616,15 @@ export function handedOf(output) {
|
|
|
616
616
|
const updatedAt = text(request.updatedAt, 40);
|
|
617
617
|
const cwd = text(request.cwd, 1000);
|
|
618
618
|
return requestId && title && updatedAt
|
|
619
|
-
? [
|
|
619
|
+
? [
|
|
620
|
+
{
|
|
621
|
+
requestId,
|
|
622
|
+
title,
|
|
623
|
+
updatedAt,
|
|
624
|
+
...(cwd ? { cwd } : {}),
|
|
625
|
+
...(request.byName === true ? { byName: true } : {}),
|
|
626
|
+
},
|
|
627
|
+
]
|
|
620
628
|
: [];
|
|
621
629
|
});
|
|
622
630
|
}
|
|
@@ -641,7 +649,7 @@ export function inTaskFolder(folders, task, home) {
|
|
|
641
649
|
});
|
|
642
650
|
}
|
|
643
651
|
export function takesHanded(handed, folders, home, now) {
|
|
644
|
-
if (!handed.cwd)
|
|
652
|
+
if (handed.byName || !handed.cwd)
|
|
645
653
|
return true;
|
|
646
654
|
if (folders.length === 0)
|
|
647
655
|
return now - Date.parse(handed.updatedAt) >= HANDED_GRACE_MS;
|
|
@@ -760,10 +768,18 @@ export function setupPrompt(connection = 'Claude Code') {
|
|
|
760
768
|
export const PRESENCE_EVERY_MS = 5 * 60_000;
|
|
761
769
|
export const PRESENCE_RUN_MS = 15_000;
|
|
762
770
|
const PRODUCTION = 'https://www.pendingyou.com';
|
|
763
|
-
export function cliConfigOf(root) {
|
|
771
|
+
export function cliConfigOf(root, name = CLI_COPY) {
|
|
772
|
+
if (name !== CLI_COPY)
|
|
773
|
+
return null;
|
|
764
774
|
return (/^(.+)[\\/]cli[\\/][^\\/]+[\\/]node_modules[\\/]pendingyou[\\/]mod[\\/]?$/.exec(root)?.[1] ??
|
|
775
|
+
/^(.+)[\\/]mod[\\/]?$/.exec(root)?.[1] ??
|
|
765
776
|
null);
|
|
766
777
|
}
|
|
778
|
+
const SESSION_FILE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
|
|
779
|
+
export function loadedPath(config, sessionId) {
|
|
780
|
+
return SESSION_FILE.test(sessionId) ? `${config}/wake/sessions/${sessionId}.json` : null;
|
|
781
|
+
}
|
|
782
|
+
export const loadedNote = (at) => `${JSON.stringify({ at: Math.floor(at) })}\n`;
|
|
767
783
|
export function originOf(manifest) {
|
|
768
784
|
let parsed = manifest;
|
|
769
785
|
if (typeof manifest === 'string')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.pendingyou.com",
|