@cmodjs/core 0.3.0 → 1.5.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/docs/commands.md +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { ContextEnd, ContextStart } from '../session/context/context.js';
|
|
2
|
+
import { Message } from '../session/context/turn/message/message.js';
|
|
3
|
+
import { Note } from '../session/context/turn/message/note.js';
|
|
4
|
+
import { Person } from '../session/context/turn/message/sender.js';
|
|
5
|
+
import { ToolResult } from '../session/context/turn/message/tool-result.js';
|
|
6
|
+
import { TurnEnd, TurnStart } from '../session/context/turn/turn.js';
|
|
7
|
+
import { GoalChange } from '../session/goal/goal.js';
|
|
8
|
+
import { TaskEnd, TaskStart } from '../session/goal/task.js';
|
|
9
|
+
import { Session } from '../session/session.js';
|
|
10
|
+
import { matches, sentMatcher } from '../utils/matcher.js';
|
|
11
|
+
import { foldAnswers } from './before.js';
|
|
12
|
+
import { cmodPluginName } from './dependencies.js';
|
|
13
|
+
export const everySession = '*';
|
|
14
|
+
export const conversationMethods = {
|
|
15
|
+
sessions: 'cmod:sessions',
|
|
16
|
+
search: 'cmod:search',
|
|
17
|
+
messages: 'cmod:messages',
|
|
18
|
+
turns: 'cmod:turns',
|
|
19
|
+
contexts: 'cmod:contexts',
|
|
20
|
+
tasks: 'cmod:tasks',
|
|
21
|
+
goal: 'cmod:goal',
|
|
22
|
+
updateGoal: 'cmod:updateGoal',
|
|
23
|
+
usage: 'cmod:usage',
|
|
24
|
+
subscribe: 'cmod:subscribe',
|
|
25
|
+
watchBefore: 'cmod:watchBefore',
|
|
26
|
+
act: 'cmod:act',
|
|
27
|
+
changed: 'cmod:changed',
|
|
28
|
+
before: 'cmod:before',
|
|
29
|
+
};
|
|
30
|
+
const actPermissions = { send: 'prompt', startTask: 'tasks', usage: 'conversation', clients: 'conversation' };
|
|
31
|
+
const startsTurns = ['CronCreate'];
|
|
32
|
+
const ownSchedule = ['CronDelete', 'CronList'];
|
|
33
|
+
export const beforePermission = (event) => (event === 'toolCall' ? 'tools' : event === TaskStart || event === TaskEnd ? 'tasks' : 'prompt');
|
|
34
|
+
export const isAct = (action) => action === 'callTool' || action in actPermissions;
|
|
35
|
+
export function toolPermission(tool) {
|
|
36
|
+
return startsTurns.includes(tool) ? 'prompt' : ownSchedule.includes(tool) ? undefined : 'tools';
|
|
37
|
+
}
|
|
38
|
+
export function actPermission(action, input) {
|
|
39
|
+
return action === 'callTool' ? (toolPermission(String(input?.name)) ?? 'tools') : actPermissions[action];
|
|
40
|
+
}
|
|
41
|
+
const asked = (action) => `cmod: ${action} runs only on a session's own main loop, and Claude Code gives a subagent's tasks no way to start a task or run a tool.`;
|
|
42
|
+
export async function usageOf(claude) {
|
|
43
|
+
const [model, usage] = await Promise.all([claude.session.model(), claude.session.usage()]);
|
|
44
|
+
const { tokens, window, percent } = usage.context;
|
|
45
|
+
return { model, context: { ...(tokens === undefined ? {} : { tokens }), window, ...(percent === undefined ? {} : { percent }) }, ...(usage.cost === undefined ? {} : { cost: { usd: usage.cost.usd } }) };
|
|
46
|
+
}
|
|
47
|
+
export function payloadOf(change) {
|
|
48
|
+
if (change.type === Message)
|
|
49
|
+
return change.message;
|
|
50
|
+
if ('turn' in change)
|
|
51
|
+
return change.turn;
|
|
52
|
+
if ('context' in change)
|
|
53
|
+
return change.context;
|
|
54
|
+
if ('task' in change) {
|
|
55
|
+
const { id, title, agentType, model, openedBy, openedAt, closedAt } = change.task;
|
|
56
|
+
return { id, title, openedBy, openedAt, ...(closedAt === undefined ? {} : { closedAt }), owner: { id, title, agentType, ...(model === undefined ? {} : { model }), startedAt: openedAt } };
|
|
57
|
+
}
|
|
58
|
+
if ('usage' in change)
|
|
59
|
+
return change.usage;
|
|
60
|
+
return change.goal;
|
|
61
|
+
}
|
|
62
|
+
export function isHeardBy(event, matcher, change) {
|
|
63
|
+
const named = change.type === Message ? event === Message || event === change.message.type : event === change.type;
|
|
64
|
+
return named && (matcher === undefined || matches(payloadOf(change), matcher));
|
|
65
|
+
}
|
|
66
|
+
function taskOfChange(change) {
|
|
67
|
+
if (change.type === Message)
|
|
68
|
+
return change.message.task;
|
|
69
|
+
if (change.type === TaskStart || change.type === TaskEnd)
|
|
70
|
+
return change.task.id === '' ? change.task.parent : change.task.id;
|
|
71
|
+
if (change.type === TurnStart || change.type === TurnEnd)
|
|
72
|
+
return change.turn.prompt.task;
|
|
73
|
+
if (change.type === ContextStart || change.type === ContextEnd)
|
|
74
|
+
return change.context.task;
|
|
75
|
+
if (change.type === GoalChange)
|
|
76
|
+
return change.goal.task;
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function ownerOfChange(change) {
|
|
80
|
+
if (change.type === TaskStart || change.type === TaskEnd)
|
|
81
|
+
return change.task.parent;
|
|
82
|
+
return taskOfChange(change);
|
|
83
|
+
}
|
|
84
|
+
function contextOfChange(change) {
|
|
85
|
+
if (change.type === Message)
|
|
86
|
+
return change.message.context;
|
|
87
|
+
if (change.type === TurnStart || change.type === TurnEnd)
|
|
88
|
+
return change.turn.context;
|
|
89
|
+
if (change.type === ContextStart || change.type === ContextEnd)
|
|
90
|
+
return change.context.number;
|
|
91
|
+
if (change.type === TaskStart || change.type === TaskEnd)
|
|
92
|
+
return change.task.context;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
export function isInScope(scope, session, change, isUnder) {
|
|
96
|
+
if (scope.session !== undefined && scope.session !== session)
|
|
97
|
+
return false;
|
|
98
|
+
const task = taskOfChange(change);
|
|
99
|
+
if (scope.task !== undefined && (task === undefined || !isUnder(task, scope.task)))
|
|
100
|
+
return false;
|
|
101
|
+
if (scope.isGoal === true && change.type !== GoalChange && change.type !== TaskStart && (task === undefined || task === scope.task))
|
|
102
|
+
return false;
|
|
103
|
+
if (scope.context !== undefined && (contextOfChange(change) !== scope.context || ownerOfChange(change) !== scope.task))
|
|
104
|
+
return false;
|
|
105
|
+
if (scope.turn === undefined)
|
|
106
|
+
return true;
|
|
107
|
+
if (change.type === Message)
|
|
108
|
+
return change.message.turn === scope.turn;
|
|
109
|
+
return (change.type === TurnStart || change.type === TurnEnd) && change.turn.prompt.id === scope.turn;
|
|
110
|
+
}
|
|
111
|
+
export function createSessionsClient({ name, claude, report, isGranted }) {
|
|
112
|
+
const call = (method, input) => claude.cmod.call({ to: cmodPluginName, method, input });
|
|
113
|
+
const listening = new Map();
|
|
114
|
+
const deciding = new Map();
|
|
115
|
+
const queued = new Set();
|
|
116
|
+
const parents = new Map();
|
|
117
|
+
const known = new Map();
|
|
118
|
+
let current = { id: '', title: '', project: '', cwd: '', model: '', startedAt: '', lastAt: '', isRunning: true };
|
|
119
|
+
const keyOf = (scope) => scope.session ?? everySession;
|
|
120
|
+
const subscribe = (key) => {
|
|
121
|
+
if (queued.has(key))
|
|
122
|
+
return;
|
|
123
|
+
queued.add(key);
|
|
124
|
+
void Promise.resolve().then(async () => {
|
|
125
|
+
queued.delete(key);
|
|
126
|
+
const heard = (from) => [...(from.get(key) ?? [])].map(({ event, matcher }) => ({ event, matcher: matcher === undefined ? undefined : sentMatcher(matcher) }));
|
|
127
|
+
const tracksTasks = [...(listening.get(key) ?? []), ...(deciding.get(key) ?? [])].some(({ scope }) => scope.task !== undefined || scope.isGoal === true);
|
|
128
|
+
const subscriptions = [...heard(listening), ...(tracksTasks ? [{ event: TaskStart }] : [])];
|
|
129
|
+
await call(conversationMethods.subscribe, { session: key, subscriptions }).catch((error) => report(`sessions:${key}`, `Hears nothing from ${key === everySession ? 'the other sessions' : `session ${key}`}`, error));
|
|
130
|
+
await call(conversationMethods.watchBefore, { session: key, subscriptions: heard(deciding) }).catch((error) => report(`before:${key}`, `Changes nothing in ${key === everySession ? 'the other sessions' : `session ${key}`}`, error));
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
const learnTasks = async (session) => {
|
|
134
|
+
if (parents.has(session))
|
|
135
|
+
return;
|
|
136
|
+
const tree = new Map();
|
|
137
|
+
parents.set(session, tree);
|
|
138
|
+
for (const task of await call(conversationMethods.tasks, { session, all: true }))
|
|
139
|
+
tree.set(task.id, task.parent);
|
|
140
|
+
};
|
|
141
|
+
const isUnderIn = (session) => (task, ancestor) => {
|
|
142
|
+
const tree = parents.get(session);
|
|
143
|
+
for (let at = task, steps = 0; at !== undefined && steps < 64; at = tree?.get(at), steps += 1)
|
|
144
|
+
if (at === ancestor)
|
|
145
|
+
return true;
|
|
146
|
+
return false;
|
|
147
|
+
};
|
|
148
|
+
const remember = (session, change) => {
|
|
149
|
+
if (change.type === TaskStart)
|
|
150
|
+
parents.get(session)?.set(change.task.id, change.task.parent);
|
|
151
|
+
};
|
|
152
|
+
const add = (into, scope, event, matcherOrListener, maybeListener, onChange) => {
|
|
153
|
+
const listener = (maybeListener ?? matcherOrListener);
|
|
154
|
+
const matcher = maybeListener === undefined ? undefined : matcherOrListener;
|
|
155
|
+
const key = keyOf(scope);
|
|
156
|
+
const entry = { scope, event, matcher, listener };
|
|
157
|
+
const set = into.get(key) ?? new Set();
|
|
158
|
+
set.add(entry);
|
|
159
|
+
into.set(key, set);
|
|
160
|
+
if (scope.task !== undefined || scope.isGoal === true)
|
|
161
|
+
void learnTasks(scope.session ?? current.id).catch((error) => report(`tasks:${key}`, 'Could not read the tasks it listens under', error));
|
|
162
|
+
onChange(key);
|
|
163
|
+
return {
|
|
164
|
+
dispose() {
|
|
165
|
+
for (const [at, held] of into) {
|
|
166
|
+
if (!held.delete(entry))
|
|
167
|
+
continue;
|
|
168
|
+
if (held.size === 0)
|
|
169
|
+
into.delete(at);
|
|
170
|
+
onChange(at);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const combined = async (session, event, change, isInProject) => {
|
|
177
|
+
const askers = [...(deciding.get(session) ?? []), ...(isInProject ? (deciding.get(everySession) ?? []) : [])]
|
|
178
|
+
.filter((entry) => entry.event === event)
|
|
179
|
+
.map((entry) => async (latest) => {
|
|
180
|
+
if (!isInScope(entry.scope, session, latest, isUnderIn(session)))
|
|
181
|
+
return undefined;
|
|
182
|
+
if (entry.matcher !== undefined && !matches(payloadOf(latest), entry.matcher))
|
|
183
|
+
return undefined;
|
|
184
|
+
const answer = await entry.listener(payloadFor(session, latest));
|
|
185
|
+
if (answer === undefined || isGranted(beforePermission(event)))
|
|
186
|
+
return answer;
|
|
187
|
+
throw new Error(`Its before(${event}) answer changes nothing until ${beforePermission(event)} is turned on for ${name} in /mods.`);
|
|
188
|
+
});
|
|
189
|
+
return foldAnswers(event, change, askers, (error) => report(`before:${event}`, `The before(${event}) listener failed`, error));
|
|
190
|
+
};
|
|
191
|
+
const recordPart = (scope) => ({
|
|
192
|
+
search: (text, options = {}) => call(conversationMethods.search, { text, ...scope, ...optionsOf(options) }),
|
|
193
|
+
on: (event, matcherOrListener, maybeListener) => add(listening, scope, event, matcherOrListener, maybeListener, subscribe),
|
|
194
|
+
before: (event, matcherOrListener, maybeListener) => add(deciding, scope, event, matcherOrListener, maybeListener, subscribe),
|
|
195
|
+
});
|
|
196
|
+
const turnOf = (scope, data) => {
|
|
197
|
+
const task = data.prompt.task ?? scope.task;
|
|
198
|
+
return {
|
|
199
|
+
...data,
|
|
200
|
+
...recordPart({ ...scope, ...(task === undefined ? {} : { task }), turn: data.prompt.id }),
|
|
201
|
+
messages: () => call(conversationMethods.messages, { session: scope.session, turn: data.prompt.id, ...(task === undefined ? {} : { task }) }),
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
const contextOf = (scope, data) => {
|
|
205
|
+
const owned = { ...(scope.session === undefined ? {} : { session: scope.session }), ...(data.task === undefined ? {} : { task: data.task }) };
|
|
206
|
+
return {
|
|
207
|
+
...data,
|
|
208
|
+
...recordPart({ ...owned, context: data.number }),
|
|
209
|
+
turns: async () => (await call(conversationMethods.turns, { ...owned, context: data.number })).map((turn) => turnOf(owned, turn)),
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
const taskOf = (session, data) => ({
|
|
213
|
+
id: data.id,
|
|
214
|
+
title: data.title,
|
|
215
|
+
openedBy: data.openedBy,
|
|
216
|
+
openedAt: data.openedAt,
|
|
217
|
+
...(data.closedAt === undefined ? {} : { closedAt: data.closedAt }),
|
|
218
|
+
owner: ownerOf(session, data),
|
|
219
|
+
...recordPart({ session, task: data.id }),
|
|
220
|
+
tasks: async () => (await call(conversationMethods.tasks, { session, task: data.id })).map((task) => taskOf(session, task)),
|
|
221
|
+
messages: () => call(conversationMethods.messages, { session, task: data.id }),
|
|
222
|
+
});
|
|
223
|
+
const goalOf = (scope, data) => ({
|
|
224
|
+
...data,
|
|
225
|
+
...recordPart({ ...scope, isGoal: true }),
|
|
226
|
+
tasks: async () => (await call(conversationMethods.tasks, { session: scope.session, ...(scope.task === undefined ? {} : { task: scope.task }) })).map((task) => taskOf(scope.session ?? current.id, task)),
|
|
227
|
+
update: async ({ text }) => {
|
|
228
|
+
if (text.trim() === '')
|
|
229
|
+
throw new Error(`${name}: a goal needs text.`);
|
|
230
|
+
await call(conversationMethods.updateGoal, { session: scope.session ?? current.id, ...(scope.task === undefined ? {} : { task: scope.task }), text });
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
const payloadFor = (session, change) => {
|
|
234
|
+
const scope = { session };
|
|
235
|
+
if (change.type === Message)
|
|
236
|
+
return change.message;
|
|
237
|
+
if ('turn' in change)
|
|
238
|
+
return turnOf(scope, change.turn);
|
|
239
|
+
if ('context' in change)
|
|
240
|
+
return contextOf(scope, change.context);
|
|
241
|
+
if ('task' in change)
|
|
242
|
+
return taskOf(session, change.task);
|
|
243
|
+
if ('usage' in change)
|
|
244
|
+
return change.usage;
|
|
245
|
+
return goalOf({ session, ...(change.goal.task === undefined ? {} : { task: change.goal.task }) }, { text: change.goal.text });
|
|
246
|
+
};
|
|
247
|
+
const act = (session, action, input, owned) => call(conversationMethods.act, { session: owned?.session ?? session, action, input, ...(owned === undefined ? {} : { task: owned.task }) });
|
|
248
|
+
const sendHere = async ({ type, text, as }) => {
|
|
249
|
+
if (type === Note) {
|
|
250
|
+
if (as !== undefined)
|
|
251
|
+
throw new Error(`${name}: a Note is always from this mod, so it takes no as.`);
|
|
252
|
+
const added = await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
|
|
253
|
+
if (added.deny !== undefined)
|
|
254
|
+
throw new Error(`${name}: Claude Code refused the note: ${added.deny}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const submitted = await claude.prompt.submit({ text, ...(as === Person ? { asUser: true } : {}) });
|
|
258
|
+
if ('drop' in submitted && typeof submitted.drop === 'string')
|
|
259
|
+
throw new Error(`${name}: Claude Code dropped the prompt: ${submitted.drop}`);
|
|
260
|
+
};
|
|
261
|
+
const startHere = async (task) => {
|
|
262
|
+
const started = await claude.agent.spawn({
|
|
263
|
+
prompt: task.prompt,
|
|
264
|
+
...(task.name === undefined ? {} : { description: task.name, name: task.name }),
|
|
265
|
+
...(task.agentType === undefined ? {} : { subagentType: task.agentType }),
|
|
266
|
+
...(task.model === undefined ? {} : { model: task.model }),
|
|
267
|
+
...(task.cwd === undefined ? {} : { cwd: task.cwd }),
|
|
268
|
+
});
|
|
269
|
+
if (started.deny !== undefined)
|
|
270
|
+
throw new Error(`${name}: Claude Code refused the task: ${started.deny}`);
|
|
271
|
+
return taskOf(current.id, { id: started.agentId ?? '', title: task.name ?? '', agentType: task.agentType ?? 'general-purpose', model: started.model, openedBy: '', openedAt: new Date().toISOString() });
|
|
272
|
+
};
|
|
273
|
+
const clientsHere = async () => [...(await claude.session.surfaces())];
|
|
274
|
+
const sessionOf = (read, owned) => {
|
|
275
|
+
const id = () => read().id;
|
|
276
|
+
const isHere = () => owned === undefined && id() === current.id;
|
|
277
|
+
const scope = () => (owned === undefined ? { session: id() } : { session: owned.session, task: owned.task });
|
|
278
|
+
const where = () => (owned === undefined ? { session: id() } : { session: owned.session, task: owned.task });
|
|
279
|
+
const turnsOf = async (options = {}) => (await call(conversationMethods.turns, { ...where(), ...options })).map((turn) => turnOf(scope(), turn));
|
|
280
|
+
const contextsOf = async () => (await call(conversationMethods.contexts, where())).map((context) => contextOf(scope(), context));
|
|
281
|
+
const session = {
|
|
282
|
+
get id() {
|
|
283
|
+
return read().id;
|
|
284
|
+
},
|
|
285
|
+
get title() {
|
|
286
|
+
return read().title;
|
|
287
|
+
},
|
|
288
|
+
get project() {
|
|
289
|
+
return read().project;
|
|
290
|
+
},
|
|
291
|
+
get agentType() {
|
|
292
|
+
return read().agentType;
|
|
293
|
+
},
|
|
294
|
+
get model() {
|
|
295
|
+
return read().model;
|
|
296
|
+
},
|
|
297
|
+
get cwd() {
|
|
298
|
+
return read().cwd;
|
|
299
|
+
},
|
|
300
|
+
get isRunning() {
|
|
301
|
+
return read().isRunning;
|
|
302
|
+
},
|
|
303
|
+
get startedAt() {
|
|
304
|
+
return read().startedAt;
|
|
305
|
+
},
|
|
306
|
+
get lastAt() {
|
|
307
|
+
return read().lastAt;
|
|
308
|
+
},
|
|
309
|
+
get forkedFrom() {
|
|
310
|
+
return read().forkedFrom;
|
|
311
|
+
},
|
|
312
|
+
search: (text, options = {}) => recordPart(scope()).search(text, options),
|
|
313
|
+
on: (event, matcherOrListener, maybeListener) => recordPart(scope()).on(event, matcherOrListener, maybeListener),
|
|
314
|
+
before: (event, matcherOrListener, maybeListener) => recordPart(scope()).before(event, matcherOrListener, maybeListener),
|
|
315
|
+
goal: async () => goalOf(scope(), await call(conversationMethods.goal, where())),
|
|
316
|
+
context: async () => {
|
|
317
|
+
const all = await contextsOf();
|
|
318
|
+
const last = all.at(-1);
|
|
319
|
+
if (last === undefined)
|
|
320
|
+
throw new Error(`${name}: session ${id()} has no context yet.`);
|
|
321
|
+
return last;
|
|
322
|
+
},
|
|
323
|
+
contexts: contextsOf,
|
|
324
|
+
turn: async () => {
|
|
325
|
+
const [last] = await turnsOf({ last: 1 });
|
|
326
|
+
return last?.closedAt === undefined ? last : undefined;
|
|
327
|
+
},
|
|
328
|
+
turns: turnsOf,
|
|
329
|
+
messages: (options = {}) => call(conversationMethods.messages, { ...where(), ...options }),
|
|
330
|
+
usage: async () => (isHere() ? usageOf(claude) : act(id(), 'usage', null, owned)),
|
|
331
|
+
clients: async () => (isHere() ? clientsHere() : act(id(), 'clients', null, owned)),
|
|
332
|
+
async send(message) {
|
|
333
|
+
if (message.text.trim() === '')
|
|
334
|
+
throw new Error(`${name}: send needs text.`);
|
|
335
|
+
if (isHere() && message.as !== Session)
|
|
336
|
+
return sendHere(message);
|
|
337
|
+
await act(id(), 'send', message, owned);
|
|
338
|
+
},
|
|
339
|
+
async startTask(task) {
|
|
340
|
+
if (owned !== undefined)
|
|
341
|
+
throw new Error(asked('startTask'));
|
|
342
|
+
if (isHere())
|
|
343
|
+
return startHere(task);
|
|
344
|
+
const started = await act(id(), 'startTask', task);
|
|
345
|
+
return taskOf(id(), started);
|
|
346
|
+
},
|
|
347
|
+
async callTool(toolCall) {
|
|
348
|
+
if (owned !== undefined)
|
|
349
|
+
throw new Error(asked('callTool'));
|
|
350
|
+
return act(id(), 'callTool', toolCall);
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
return session;
|
|
354
|
+
};
|
|
355
|
+
const ownerOf = (session, data) => {
|
|
356
|
+
const parent = session === current.id ? current : known.get(session);
|
|
357
|
+
return sessionOf(() => ({
|
|
358
|
+
id: data.id,
|
|
359
|
+
title: data.title,
|
|
360
|
+
project: parent?.project ?? '',
|
|
361
|
+
agentType: data.agentType,
|
|
362
|
+
model: data.model ?? '',
|
|
363
|
+
cwd: parent?.cwd ?? '',
|
|
364
|
+
isRunning: data.closedAt === undefined && (parent?.isRunning ?? true),
|
|
365
|
+
startedAt: data.openedAt,
|
|
366
|
+
lastAt: data.closedAt ?? data.openedAt,
|
|
367
|
+
}), { session, task: data.id });
|
|
368
|
+
};
|
|
369
|
+
const currentSession = sessionOf(() => current);
|
|
370
|
+
const every = recordPart({});
|
|
371
|
+
const answerBefore = async (session, event, change, isInProject) => (isGranted('conversation') ? combined(session, event, change, isInProject) : undefined);
|
|
372
|
+
return {
|
|
373
|
+
sessions: {
|
|
374
|
+
get current() {
|
|
375
|
+
return currentSession;
|
|
376
|
+
},
|
|
377
|
+
list: async (scope = {}) => (await call(conversationMethods.sessions, scope)).map((data) => {
|
|
378
|
+
if (data.id !== current.id) {
|
|
379
|
+
known.set(data.id, data);
|
|
380
|
+
return sessionOf(() => known.get(data.id) ?? data);
|
|
381
|
+
}
|
|
382
|
+
if (current.title === '')
|
|
383
|
+
current = { ...current, title: data.title };
|
|
384
|
+
return currentSession;
|
|
385
|
+
}),
|
|
386
|
+
search: (text, options = {}) => call(conversationMethods.search, { text, ...optionsOf(options), ...(options.running === undefined ? {} : { running: options.running }), ...(options.since === undefined ? {} : { since: options.since }), ...(options.everyProject === undefined ? {} : { everyProject: options.everyProject }) }),
|
|
387
|
+
on: every.on,
|
|
388
|
+
before: every.before,
|
|
389
|
+
},
|
|
390
|
+
changed(session, change, isInProject) {
|
|
391
|
+
remember(session, change);
|
|
392
|
+
for (const key of isInProject ? [session, everySession] : [session]) {
|
|
393
|
+
for (const { scope, event, matcher, listener } of listening.get(key) ?? []) {
|
|
394
|
+
if (!isHeardBy(event, matcher, change) || !isInScope(scope, session, change, isUnderIn(session)))
|
|
395
|
+
continue;
|
|
396
|
+
void Promise.resolve()
|
|
397
|
+
.then(() => listener(payloadFor(session, change)))
|
|
398
|
+
.catch((error) => report(`session:${event}`, `The ${event} listener failed`, error));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
answerBefore,
|
|
403
|
+
follow(next) {
|
|
404
|
+
const moved = next.id !== current.id && current.id !== '';
|
|
405
|
+
const previous = current.id;
|
|
406
|
+
current = { ...next, title: next.title || (next.id === current.id ? current.title : ''), isRunning: true, lastAt: new Date().toISOString() };
|
|
407
|
+
if (!moved)
|
|
408
|
+
return;
|
|
409
|
+
for (const into of [listening, deciding]) {
|
|
410
|
+
const kept = into.get(previous);
|
|
411
|
+
if (kept === undefined)
|
|
412
|
+
continue;
|
|
413
|
+
into.delete(previous);
|
|
414
|
+
for (const entry of kept)
|
|
415
|
+
if (entry.scope.session === previous)
|
|
416
|
+
entry.scope = { ...entry.scope, session: current.id };
|
|
417
|
+
into.set(current.id, new Set([...(into.get(current.id) ?? []), ...kept]));
|
|
418
|
+
}
|
|
419
|
+
parents.delete(previous);
|
|
420
|
+
subscribe(previous);
|
|
421
|
+
subscribe(current.id);
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export function resultOf(where, answered) {
|
|
426
|
+
const base = { id: '', type: ToolResult, at: new Date().toISOString(), session: where.session, context: where.context, ...(where.turn === undefined ? {} : { turn: where.turn.prompt.id }), callId: '' };
|
|
427
|
+
if (answered.deny !== undefined)
|
|
428
|
+
return { ...base, text: answered.deny, isError: true };
|
|
429
|
+
return { ...base, text: answered.text ?? JSON.stringify(answered.result), isError: answered.isError === true };
|
|
430
|
+
}
|
|
431
|
+
function optionsOf(options) {
|
|
432
|
+
return { ...(options.limit === undefined ? {} : { limit: options.limit }), ...(options.before === undefined ? {} : { before: { session: options.before.session, id: options.before.id } }) };
|
|
433
|
+
}
|
package/runtime/state.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ClassicHookInputs } from 'claude-code';
|
|
2
2
|
import type { Claude } from './claude.js';
|
|
3
|
+
import type { Failure } from './toaster.js';
|
|
3
4
|
export type StateOptions = {
|
|
4
5
|
readonly name: string;
|
|
5
6
|
readonly initial: object;
|
|
6
7
|
readonly session: string;
|
|
7
8
|
readonly root: string;
|
|
8
9
|
readonly claude: Claude;
|
|
10
|
+
readonly report: (failure: Failure) => void;
|
|
9
11
|
readonly changed: () => void;
|
|
12
|
+
readonly forkParent: () => Promise<string | undefined>;
|
|
10
13
|
};
|
|
11
14
|
export type ModState<State extends object> = {
|
|
12
15
|
readonly state: State;
|
|
@@ -14,8 +17,9 @@ export type ModState<State extends object> = {
|
|
|
14
17
|
changed(): void;
|
|
15
18
|
load(): Promise<void>;
|
|
16
19
|
moveTo(root: string): Promise<void>;
|
|
17
|
-
switchSession(session: string, source: ClassicHookInputs['SessionStart']['source']): Promise<void>;
|
|
20
|
+
switchSession(session: string, source: ClassicHookInputs['SessionStart']['source'], transcript: string): Promise<void>;
|
|
18
21
|
};
|
|
19
22
|
export declare function onStateChange(state: object, listener: () => void): void;
|
|
20
|
-
export declare function
|
|
23
|
+
export declare function sessionsWith(claude: Pick<Claude, 'store'>, name: string, key: string, value: unknown): Promise<string[]>;
|
|
24
|
+
export declare function createState<State extends object>({ name, initial, session, root, claude, report, changed: redraw, forkParent }: StateOptions): ModState<State>;
|
|
21
25
|
//# sourceMappingURL=state.d.ts.map
|
package/runtime/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAQ3C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,IAAI;IAC3C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvH,CAAA;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO9H;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,CA6G9J"}
|
package/runtime/state.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { messageOf } from '../utils/text.js';
|
|
2
1
|
const lifetimes = ['memory', 'session', 'project', 'global'];
|
|
3
|
-
const
|
|
2
|
+
const keptProjects = 20;
|
|
4
3
|
const listeners = new WeakMap();
|
|
5
4
|
export function onStateChange(state, listener) {
|
|
6
5
|
const own = listeners.get(state) ?? new Set();
|
|
7
6
|
own.add(listener);
|
|
8
7
|
listeners.set(state, own);
|
|
9
8
|
}
|
|
10
|
-
export function
|
|
9
|
+
export async function sessionsWith(claude, name, key, value) {
|
|
10
|
+
const prefix = `${name}.${key}.sessions.`;
|
|
11
|
+
const found = [];
|
|
12
|
+
for (const stored of (await claude.store.keys()).filter((each) => each.startsWith(prefix))) {
|
|
13
|
+
if ((await claude.store.get(stored)) === value)
|
|
14
|
+
found.push(stored.slice(prefix.length));
|
|
15
|
+
}
|
|
16
|
+
return found;
|
|
17
|
+
}
|
|
18
|
+
export function createState({ name, initial, session, root, claude, report, changed: redraw, forkParent }) {
|
|
11
19
|
const changed = () => {
|
|
12
20
|
redraw();
|
|
13
21
|
for (const listener of listeners.get(state) ?? [])
|
|
@@ -20,21 +28,36 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
20
28
|
let turn = Promise.resolve();
|
|
21
29
|
const inTurn = (task) => {
|
|
22
30
|
const run = turn.then(task);
|
|
23
|
-
turn =
|
|
31
|
+
turn = Promise.allSettled([run]);
|
|
24
32
|
return run;
|
|
25
33
|
};
|
|
26
34
|
const ownersKey = (lifetime, key) => `${name}.${key}.${lifetime === 'project' ? 'projects' : 'sessions'}.`;
|
|
27
35
|
const storeKey = (lifetime, key, owner) => (lifetime === 'global' ? `${name}.${key}` : `${ownersKey(lifetime, key)}${owner}`);
|
|
28
36
|
const read = (lifetime, key, owner) => claude.store.get(storeKey(lifetime, key, owner));
|
|
29
37
|
const write = async (lifetime, key, value, owner) => {
|
|
30
|
-
if (lifetime
|
|
38
|
+
if (lifetime !== 'project')
|
|
31
39
|
return claude.store.set(storeKey(lifetime, key, owner), value);
|
|
32
40
|
await claude.store.delete(storeKey(lifetime, key, owner));
|
|
33
41
|
await claude.store.set(storeKey(lifetime, key, owner), value);
|
|
34
42
|
const owners = (await claude.store.keys()).filter((stored) => stored.startsWith(ownersKey(lifetime, key)));
|
|
35
|
-
for (const oldest of owners.slice(0, -
|
|
43
|
+
for (const oldest of owners.slice(0, -keptProjects))
|
|
36
44
|
await claude.store.delete(oldest);
|
|
37
45
|
};
|
|
46
|
+
const transcriptsKey = `${name}.$transcripts.`;
|
|
47
|
+
const forgetEndedSessions = async (current) => {
|
|
48
|
+
const stored = await claude.store.keys();
|
|
49
|
+
for (const recorded of stored.filter((key) => key.startsWith(transcriptsKey))) {
|
|
50
|
+
const owner = recorded.slice(transcriptsKey.length);
|
|
51
|
+
if (owner === current)
|
|
52
|
+
continue;
|
|
53
|
+
const transcript = await claude.store.get(recorded);
|
|
54
|
+
if (typeof transcript === 'string' && (await claude.fs.exists(transcript)))
|
|
55
|
+
continue;
|
|
56
|
+
for (const value of stored.filter((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`)))
|
|
57
|
+
await claude.store.delete(value);
|
|
58
|
+
await claude.store.delete(recorded);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
38
61
|
const loadGroups = async (only, next) => {
|
|
39
62
|
const loaded = [];
|
|
40
63
|
for (const lifetime of only) {
|
|
@@ -47,13 +70,14 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
47
70
|
current[lifetime][key] = saved === undefined ? declared[lifetime][key] : frozen(name, `${lifetime}.${key}`, saved);
|
|
48
71
|
changed();
|
|
49
72
|
};
|
|
50
|
-
const copySession = async (
|
|
73
|
+
const copySession = async (from, to) => {
|
|
51
74
|
for (const key of Object.keys(declared.session)) {
|
|
52
|
-
const saved = await read('session', key,
|
|
75
|
+
const saved = await read('session', key, from);
|
|
53
76
|
if (saved !== undefined)
|
|
54
|
-
await write('session', key, saved,
|
|
77
|
+
await write('session', key, saved, to);
|
|
55
78
|
}
|
|
56
79
|
};
|
|
80
|
+
const hasSaved = async (owner) => (await claude.store.keys()).some((key) => key.startsWith(`${name}.`) && key.endsWith(`.sessions.${owner}`));
|
|
57
81
|
const group = (lifetime) => new Proxy(current[lifetime], {
|
|
58
82
|
set(target, key, value) {
|
|
59
83
|
if (typeof key !== 'string')
|
|
@@ -66,7 +90,7 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
66
90
|
if (lifetime !== 'memory') {
|
|
67
91
|
const owner = lifetime === 'project' ? projectRoot : sessionId;
|
|
68
92
|
inTurn(() => write(lifetime, key, value, owner)).catch((error) => {
|
|
69
|
-
|
|
93
|
+
report({ id: `state:${lifetime}.${key}`, message: `Could not keep state.${lifetime}.${key}. Keep only JSON data in mod.state`, error });
|
|
70
94
|
});
|
|
71
95
|
}
|
|
72
96
|
changed();
|
|
@@ -80,13 +104,22 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
80
104
|
return projectRoot;
|
|
81
105
|
},
|
|
82
106
|
changed,
|
|
83
|
-
load: () => inTurn(() =>
|
|
107
|
+
load: () => inTurn(async () => {
|
|
108
|
+
if (Object.keys(declared.session).length > 0 && !(await hasSaved(sessionId))) {
|
|
109
|
+
const parent = await forkParent();
|
|
110
|
+
if (parent !== undefined)
|
|
111
|
+
await copySession(parent, sessionId);
|
|
112
|
+
}
|
|
113
|
+
return loadGroups(lifetimes, { root: projectRoot, session: sessionId });
|
|
114
|
+
}),
|
|
84
115
|
moveTo: (nextRoot) => inTurn(async () => (nextRoot === projectRoot ? undefined : loadGroups(['project'], { root: nextRoot, session: sessionId }))),
|
|
85
|
-
switchSession: (nextSession, source) => inTurn(async () => {
|
|
116
|
+
switchSession: (nextSession, source, transcript) => inTurn(async () => {
|
|
117
|
+
await claude.store.set(`${transcriptsKey}${nextSession}`, transcript);
|
|
118
|
+
await forgetEndedSessions(nextSession);
|
|
86
119
|
if (nextSession === sessionId)
|
|
87
120
|
return;
|
|
88
121
|
if (source === 'fork')
|
|
89
|
-
await copySession(nextSession);
|
|
122
|
+
await copySession(sessionId, nextSession);
|
|
90
123
|
return loadGroups(['memory', 'session'], { root: projectRoot, session: nextSession });
|
|
91
124
|
}),
|
|
92
125
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ElementTable, RenderElement, RenderPropsOf } from 'claude-code';
|
|
2
|
+
import type { Toast, ToastId, ToastType } from '../mod.js';
|
|
3
|
+
import type { Claude } from './claude.js';
|
|
4
|
+
export type ShownToast = {
|
|
5
|
+
readonly type: ToastType;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
};
|
|
9
|
+
export type Failure = {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly error?: unknown;
|
|
13
|
+
readonly description?: string;
|
|
14
|
+
readonly retry?: () => unknown;
|
|
15
|
+
};
|
|
16
|
+
export type Toaster = {
|
|
17
|
+
readonly toast: Toast;
|
|
18
|
+
copy(text: string): Promise<void>;
|
|
19
|
+
fail(failure: Failure): ToastId;
|
|
20
|
+
readonly shown: readonly ShownToast[];
|
|
21
|
+
draw(drawn: RenderElement, table: ElementTable, band: RenderPropsOf['AbovePrompt'], bandId: string): RenderElement;
|
|
22
|
+
};
|
|
23
|
+
export declare function createToaster(claude: Claude, name: string, version: () => string | undefined): Toaster;
|
|
24
|
+
//# sourceMappingURL=toaster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toaster.d.ts","sourceRoot":"","sources":["../src/runtime/toaster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAS,MAAM,aAAa,CAAA;AACpF,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAgB,SAAS,EAAE,MAAM,WAAW,CAAA;AAIxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9G,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,CAAA;CAAE,CAAA;AAEhK,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;CACnH,CAAA;AAsCD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAwOtG"}
|