@lumenflow/cli 3.6.6 → 3.6.7
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/dist/init.js +78 -7
- package/dist/init.js.map +1 -1
- package/dist/initiative-create.js +74 -23
- package/dist/initiative-create.js.map +1 -1
- package/dist/lane-lock.js +62 -1
- package/dist/lane-lock.js.map +1 -1
- package/dist/lane-setup.js +102 -28
- package/dist/lane-setup.js.map +1 -1
- package/dist/lane-status.js +42 -0
- package/dist/lane-status.js.map +1 -1
- package/dist/lane-validate.js +62 -1
- package/dist/lane-validate.js.map +1 -1
- package/dist/plan-link.js +25 -2
- package/dist/plan-link.js.map +1 -1
- package/dist/public-manifest.js +7 -0
- package/dist/public-manifest.js.map +1 -1
- package/dist/release.js +17 -0
- package/dist/release.js.map +1 -1
- package/dist/state-doctor-fix.js +12 -11
- package/dist/state-doctor-fix.js.map +1 -1
- package/dist/state-emit.js +198 -0
- package/dist/state-emit.js.map +1 -0
- package/dist/wu-claim-state.js +58 -15
- package/dist/wu-claim-state.js.map +1 -1
- package/dist/wu-claim-worktree.js +3 -3
- package/dist/wu-claim-worktree.js.map +1 -1
- package/dist/wu-claim.js +19 -1
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-create-content.js +2 -4
- package/dist/wu-create-content.js.map +1 -1
- package/dist/wu-create-validation.js +14 -1
- package/dist/wu-create-validation.js.map +1 -1
- package/dist/wu-create.js +2 -6
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +95 -4
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-edit-operations.js +36 -5
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-recover.js +115 -12
- package/dist/wu-recover.js.map +1 -1
- package/package.json +9 -8
- package/packs/sidekick/.turbo/turbo-build.log +4 -0
- package/packs/sidekick/README.md +194 -0
- package/packs/sidekick/constants.ts +10 -0
- package/packs/sidekick/index.ts +8 -0
- package/packs/sidekick/manifest-schema.ts +262 -0
- package/packs/sidekick/manifest.ts +333 -0
- package/packs/sidekick/manifest.yaml +406 -0
- package/packs/sidekick/pack-registration.ts +110 -0
- package/packs/sidekick/package.json +55 -0
- package/packs/sidekick/tool-impl/channel-tools.ts +226 -0
- package/packs/sidekick/tool-impl/index.ts +22 -0
- package/packs/sidekick/tool-impl/memory-tools.ts +188 -0
- package/packs/sidekick/tool-impl/routine-tools.ts +194 -0
- package/packs/sidekick/tool-impl/shared.ts +124 -0
- package/packs/sidekick/tool-impl/storage.ts +315 -0
- package/packs/sidekick/tool-impl/system-tools.ts +155 -0
- package/packs/sidekick/tool-impl/task-tools.ts +278 -0
- package/packs/sidekick/tools/channel-tools.ts +53 -0
- package/packs/sidekick/tools/index.ts +9 -0
- package/packs/sidekick/tools/memory-tools.ts +53 -0
- package/packs/sidekick/tools/routine-tools.ts +53 -0
- package/packs/sidekick/tools/system-tools.ts +47 -0
- package/packs/sidekick/tools/task-tools.ts +61 -0
- package/packs/sidekick/tools/types.ts +57 -0
- package/packs/sidekick/tsconfig.json +20 -0
- package/templates/core/ai/onboarding/starting-prompt.md.template +33 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import { getStoragePort, type ChannelMessageRecord, type ChannelRecord } from './storage.js';
|
|
5
|
+
import {
|
|
6
|
+
asInteger,
|
|
7
|
+
asNonEmptyString,
|
|
8
|
+
buildAuditEvent,
|
|
9
|
+
createId,
|
|
10
|
+
failure,
|
|
11
|
+
isDryRun,
|
|
12
|
+
nowIso,
|
|
13
|
+
success,
|
|
14
|
+
toRecord,
|
|
15
|
+
type ToolContextLike,
|
|
16
|
+
type ToolOutput,
|
|
17
|
+
} from './shared.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const TOOL_NAMES = {
|
|
24
|
+
CONFIGURE: 'channel:configure',
|
|
25
|
+
SEND: 'channel:send',
|
|
26
|
+
RECEIVE: 'channel:receive',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
const OUTBOX_CAP = 100;
|
|
30
|
+
const DEFAULT_CHANNEL_NAME = 'default';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// channel:configure
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
async function channelConfigureTool(
|
|
37
|
+
input: unknown,
|
|
38
|
+
context?: ToolContextLike,
|
|
39
|
+
): Promise<ToolOutput> {
|
|
40
|
+
const parsed = toRecord(input);
|
|
41
|
+
const name = asNonEmptyString(parsed.name);
|
|
42
|
+
|
|
43
|
+
if (!name) {
|
|
44
|
+
return failure('INVALID_INPUT', 'name is required.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const now = nowIso();
|
|
48
|
+
const channel: ChannelRecord = {
|
|
49
|
+
id: createId('chan'),
|
|
50
|
+
name,
|
|
51
|
+
created_at: now,
|
|
52
|
+
updated_at: now,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (isDryRun(parsed)) {
|
|
56
|
+
return success({
|
|
57
|
+
dry_run: true,
|
|
58
|
+
channel: channel as unknown as Record<string, unknown>,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const storage = getStoragePort();
|
|
63
|
+
let resolvedChannel = channel;
|
|
64
|
+
|
|
65
|
+
await storage.withLock(async () => {
|
|
66
|
+
const channels = await storage.readStore('channels');
|
|
67
|
+
const existing = channels.find((c) => c.name === name);
|
|
68
|
+
|
|
69
|
+
if (existing) {
|
|
70
|
+
resolvedChannel = { ...existing, updated_at: now };
|
|
71
|
+
const updated = channels.map((c) => (c.id === existing.id ? resolvedChannel : c));
|
|
72
|
+
await storage.writeStore('channels', updated);
|
|
73
|
+
await storage.appendAudit(
|
|
74
|
+
buildAuditEvent({
|
|
75
|
+
tool: TOOL_NAMES.CONFIGURE,
|
|
76
|
+
op: 'update',
|
|
77
|
+
context,
|
|
78
|
+
ids: [existing.id],
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
channels.push(channel);
|
|
83
|
+
await storage.writeStore('channels', channels);
|
|
84
|
+
await storage.appendAudit(
|
|
85
|
+
buildAuditEvent({
|
|
86
|
+
tool: TOOL_NAMES.CONFIGURE,
|
|
87
|
+
op: 'create',
|
|
88
|
+
context,
|
|
89
|
+
ids: [channel.id],
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return success({ channel: resolvedChannel as unknown as Record<string, unknown> });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// channel:send
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
async function channelSendTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
|
|
103
|
+
const parsed = toRecord(input);
|
|
104
|
+
const content = asNonEmptyString(parsed.content);
|
|
105
|
+
|
|
106
|
+
if (!content) {
|
|
107
|
+
return failure('INVALID_INPUT', 'content is required.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const channelName = asNonEmptyString(parsed.channel) ?? DEFAULT_CHANNEL_NAME;
|
|
111
|
+
const sender = asNonEmptyString(parsed.sender) ?? 'assistant';
|
|
112
|
+
const now = nowIso();
|
|
113
|
+
|
|
114
|
+
const message: ChannelMessageRecord = {
|
|
115
|
+
id: createId('msg'),
|
|
116
|
+
channel_id: '', // resolved below
|
|
117
|
+
sender,
|
|
118
|
+
content,
|
|
119
|
+
created_at: now,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (isDryRun(parsed)) {
|
|
123
|
+
return success({
|
|
124
|
+
dry_run: true,
|
|
125
|
+
message: { ...message, channel_id: 'dry-run' } as unknown as Record<string, unknown>,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const storage = getStoragePort();
|
|
130
|
+
let resolvedMessage = message;
|
|
131
|
+
|
|
132
|
+
await storage.withLock(async () => {
|
|
133
|
+
const channels = await storage.readStore('channels');
|
|
134
|
+
const found = channels.find((c) => c.name === channelName);
|
|
135
|
+
let channelId: string;
|
|
136
|
+
|
|
137
|
+
if (!found) {
|
|
138
|
+
channelId = createId('chan');
|
|
139
|
+
channels.push({
|
|
140
|
+
id: channelId,
|
|
141
|
+
name: channelName,
|
|
142
|
+
created_at: now,
|
|
143
|
+
updated_at: now,
|
|
144
|
+
});
|
|
145
|
+
await storage.writeStore('channels', channels);
|
|
146
|
+
} else {
|
|
147
|
+
channelId = found.id;
|
|
148
|
+
const updated = channels.map((c) => (c.id === channelId ? { ...c, updated_at: now } : c));
|
|
149
|
+
await storage.writeStore('channels', updated);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
resolvedMessage = { ...message, channel_id: channelId };
|
|
153
|
+
|
|
154
|
+
const messages = await storage.readStore('messages');
|
|
155
|
+
messages.push(resolvedMessage);
|
|
156
|
+
|
|
157
|
+
// Cap outbox at OUTBOX_CAP
|
|
158
|
+
const capped = messages.length > OUTBOX_CAP ? messages.slice(-OUTBOX_CAP) : messages;
|
|
159
|
+
await storage.writeStore('messages', capped);
|
|
160
|
+
|
|
161
|
+
await storage.appendAudit(
|
|
162
|
+
buildAuditEvent({
|
|
163
|
+
tool: TOOL_NAMES.SEND,
|
|
164
|
+
op: 'create',
|
|
165
|
+
context,
|
|
166
|
+
ids: [channelId, resolvedMessage.id],
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return success({ message: resolvedMessage as unknown as Record<string, unknown> });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// channel:receive
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
async function channelReceiveTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
|
|
179
|
+
const parsed = toRecord(input);
|
|
180
|
+
const channelName = asNonEmptyString(parsed.channel);
|
|
181
|
+
const limit = asInteger(parsed.limit);
|
|
182
|
+
|
|
183
|
+
const storage = getStoragePort();
|
|
184
|
+
const messages = await storage.readStore('messages');
|
|
185
|
+
const channels = await storage.readStore('channels');
|
|
186
|
+
|
|
187
|
+
let filtered = messages;
|
|
188
|
+
|
|
189
|
+
if (channelName) {
|
|
190
|
+
const channel = channels.find((c) => c.name === channelName);
|
|
191
|
+
if (channel) {
|
|
192
|
+
filtered = messages.filter((m) => m.channel_id === channel.id);
|
|
193
|
+
} else {
|
|
194
|
+
filtered = [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sorted = filtered.toSorted((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
199
|
+
|
|
200
|
+
const items = limit && limit > 0 ? sorted.slice(-limit) : sorted;
|
|
201
|
+
|
|
202
|
+
return success({
|
|
203
|
+
items: items as unknown as Record<string, unknown>,
|
|
204
|
+
count: items.length,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Router (default export)
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
export default async function channelTools(
|
|
213
|
+
input: unknown,
|
|
214
|
+
context?: ToolContextLike,
|
|
215
|
+
): Promise<ToolOutput> {
|
|
216
|
+
switch (context?.tool_name) {
|
|
217
|
+
case TOOL_NAMES.CONFIGURE:
|
|
218
|
+
return channelConfigureTool(input, context);
|
|
219
|
+
case TOOL_NAMES.SEND:
|
|
220
|
+
return channelSendTool(input, context);
|
|
221
|
+
case TOOL_NAMES.RECEIVE:
|
|
222
|
+
return channelReceiveTool(input, context);
|
|
223
|
+
default:
|
|
224
|
+
return failure('UNKNOWN_TOOL', `Unknown channel tool: ${context?.tool_name ?? 'unknown'}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
type AuditEvent,
|
|
6
|
+
type ChannelMessageRecord,
|
|
7
|
+
type ChannelRecord,
|
|
8
|
+
type MemoryRecord,
|
|
9
|
+
type MemoryType,
|
|
10
|
+
type RoutineRecord,
|
|
11
|
+
type RoutineStepRecord,
|
|
12
|
+
type SidekickStores,
|
|
13
|
+
type StoragePort,
|
|
14
|
+
type StoreName,
|
|
15
|
+
type TaskPriority,
|
|
16
|
+
type TaskRecord,
|
|
17
|
+
type TaskStatus,
|
|
18
|
+
FsStoragePort,
|
|
19
|
+
getStoragePort,
|
|
20
|
+
runWithStoragePort,
|
|
21
|
+
setDefaultStoragePort,
|
|
22
|
+
} from './storage.js';
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import { getStoragePort, type MemoryRecord, type MemoryType } from './storage.js';
|
|
5
|
+
import {
|
|
6
|
+
asInteger,
|
|
7
|
+
asNonEmptyString,
|
|
8
|
+
asStringArray,
|
|
9
|
+
buildAuditEvent,
|
|
10
|
+
createId,
|
|
11
|
+
failure,
|
|
12
|
+
includesText,
|
|
13
|
+
isDryRun,
|
|
14
|
+
matchesTags,
|
|
15
|
+
nowIso,
|
|
16
|
+
success,
|
|
17
|
+
toRecord,
|
|
18
|
+
type ToolContextLike,
|
|
19
|
+
type ToolOutput,
|
|
20
|
+
} from './shared.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const TOOL_NAMES = {
|
|
27
|
+
STORE: 'memory:store',
|
|
28
|
+
RECALL: 'memory:recall',
|
|
29
|
+
FORGET: 'memory:forget',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const VALID_MEMORY_TYPES: MemoryType[] = ['fact', 'preference', 'note'];
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
function asMemoryType(value: unknown): MemoryType | null {
|
|
39
|
+
return VALID_MEMORY_TYPES.includes(value as MemoryType) ? (value as MemoryType) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// memory:store
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
async function memoryStoreTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
|
|
47
|
+
const parsed = toRecord(input);
|
|
48
|
+
const type = asMemoryType(parsed.type);
|
|
49
|
+
const content = asNonEmptyString(parsed.content);
|
|
50
|
+
|
|
51
|
+
if (!type) {
|
|
52
|
+
return failure('INVALID_INPUT', 'type must be one of fact, preference, note.');
|
|
53
|
+
}
|
|
54
|
+
if (!content) {
|
|
55
|
+
return failure('INVALID_INPUT', 'content is required.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const memory: MemoryRecord = {
|
|
59
|
+
id: createId('mem'),
|
|
60
|
+
type,
|
|
61
|
+
content,
|
|
62
|
+
tags: asStringArray(parsed.tags),
|
|
63
|
+
created_at: nowIso(),
|
|
64
|
+
updated_at: nowIso(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (isDryRun(parsed)) {
|
|
68
|
+
return success({
|
|
69
|
+
dry_run: true,
|
|
70
|
+
memory: memory as unknown as Record<string, unknown>,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const storage = getStoragePort();
|
|
75
|
+
await storage.withLock(async () => {
|
|
76
|
+
const memories = await storage.readStore('memories');
|
|
77
|
+
memories.push(memory);
|
|
78
|
+
await storage.writeStore('memories', memories);
|
|
79
|
+
await storage.appendAudit(
|
|
80
|
+
buildAuditEvent({
|
|
81
|
+
tool: TOOL_NAMES.STORE,
|
|
82
|
+
op: 'create',
|
|
83
|
+
context,
|
|
84
|
+
ids: [memory.id],
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return success({ memory: memory as unknown as Record<string, unknown> });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// memory:recall
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
async function memoryRecallTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
|
|
97
|
+
const parsed = toRecord(input);
|
|
98
|
+
const query = asNonEmptyString(parsed.query);
|
|
99
|
+
const type = asMemoryType(parsed.type);
|
|
100
|
+
const tags = asStringArray(parsed.tags);
|
|
101
|
+
const limit = asInteger(parsed.limit);
|
|
102
|
+
|
|
103
|
+
const storage = getStoragePort();
|
|
104
|
+
const memories = await storage.readStore('memories');
|
|
105
|
+
|
|
106
|
+
const filtered = memories.filter((memory) => {
|
|
107
|
+
if (type && memory.type !== type) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!matchesTags(tags, memory.tags)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (!includesText(memory.content, query)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const sorted = filtered.toSorted((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
|
|
120
|
+
|
|
121
|
+
const items = limit && limit > 0 ? sorted.slice(0, limit) : sorted;
|
|
122
|
+
|
|
123
|
+
return success({
|
|
124
|
+
items: items as unknown as Record<string, unknown>,
|
|
125
|
+
count: items.length,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// memory:forget
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
async function memoryForgetTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
|
|
134
|
+
const parsed = toRecord(input);
|
|
135
|
+
const id = asNonEmptyString(parsed.id);
|
|
136
|
+
|
|
137
|
+
if (!id) {
|
|
138
|
+
return failure('INVALID_INPUT', 'id is required.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const storage = getStoragePort();
|
|
142
|
+
const memories = await storage.readStore('memories');
|
|
143
|
+
const exists = memories.some((memory) => memory.id === id);
|
|
144
|
+
|
|
145
|
+
if (!exists) {
|
|
146
|
+
return failure('NOT_FOUND', `memory ${id} was not found.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isDryRun(parsed)) {
|
|
150
|
+
return success({ dry_run: true, deleted_id: id });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await storage.withLock(async () => {
|
|
154
|
+
const latest = await storage.readStore('memories');
|
|
155
|
+
const remaining = latest.filter((memory) => memory.id !== id);
|
|
156
|
+
await storage.writeStore('memories', remaining);
|
|
157
|
+
await storage.appendAudit(
|
|
158
|
+
buildAuditEvent({
|
|
159
|
+
tool: TOOL_NAMES.FORGET,
|
|
160
|
+
op: 'delete',
|
|
161
|
+
context,
|
|
162
|
+
ids: [id],
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return success({ deleted_id: id });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Router (default export)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
export default async function memoryTools(
|
|
175
|
+
input: unknown,
|
|
176
|
+
context?: ToolContextLike,
|
|
177
|
+
): Promise<ToolOutput> {
|
|
178
|
+
switch (context?.tool_name) {
|
|
179
|
+
case TOOL_NAMES.STORE:
|
|
180
|
+
return memoryStoreTool(input, context);
|
|
181
|
+
case TOOL_NAMES.RECALL:
|
|
182
|
+
return memoryRecallTool(input, context);
|
|
183
|
+
case TOOL_NAMES.FORGET:
|
|
184
|
+
return memoryForgetTool(input, context);
|
|
185
|
+
default:
|
|
186
|
+
return failure('UNKNOWN_TOOL', `Unknown memory tool: ${context?.tool_name ?? 'unknown'}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hellmai Ltd
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import { getStoragePort, type RoutineRecord, type RoutineStepRecord } from './storage.js';
|
|
5
|
+
import {
|
|
6
|
+
asInteger,
|
|
7
|
+
asNonEmptyString,
|
|
8
|
+
buildAuditEvent,
|
|
9
|
+
createId,
|
|
10
|
+
failure,
|
|
11
|
+
isDryRun,
|
|
12
|
+
nowIso,
|
|
13
|
+
success,
|
|
14
|
+
toRecord,
|
|
15
|
+
type ToolContextLike,
|
|
16
|
+
type ToolOutput,
|
|
17
|
+
} from './shared.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const TOOL_NAMES = {
|
|
24
|
+
CREATE: 'routine:create',
|
|
25
|
+
LIST: 'routine:list',
|
|
26
|
+
RUN: 'routine:run',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function normalizeSteps(value: unknown): RoutineStepRecord[] {
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const steps: RoutineStepRecord[] = [];
|
|
39
|
+
for (const candidate of value) {
|
|
40
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const record = candidate as Record<string, unknown>;
|
|
44
|
+
const tool = asNonEmptyString(record.tool);
|
|
45
|
+
if (!tool) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const input =
|
|
49
|
+
record.input && typeof record.input === 'object' && !Array.isArray(record.input)
|
|
50
|
+
? (record.input as Record<string, unknown>)
|
|
51
|
+
: {};
|
|
52
|
+
|
|
53
|
+
steps.push({ tool, input });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return steps;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// routine:create
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
async function routineCreateTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
|
|
64
|
+
const parsed = toRecord(input);
|
|
65
|
+
const name = asNonEmptyString(parsed.name);
|
|
66
|
+
const steps = normalizeSteps(parsed.steps);
|
|
67
|
+
|
|
68
|
+
if (!name) {
|
|
69
|
+
return failure('INVALID_INPUT', 'name is required.');
|
|
70
|
+
}
|
|
71
|
+
if (steps.length === 0) {
|
|
72
|
+
return failure('INVALID_INPUT', 'steps must include at least one tool step.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const now = nowIso();
|
|
76
|
+
const routine: RoutineRecord = {
|
|
77
|
+
id: createId('routine'),
|
|
78
|
+
name,
|
|
79
|
+
steps,
|
|
80
|
+
created_at: now,
|
|
81
|
+
updated_at: now,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (isDryRun(parsed)) {
|
|
85
|
+
return success({
|
|
86
|
+
dry_run: true,
|
|
87
|
+
routine: routine as unknown as Record<string, unknown>,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const storage = getStoragePort();
|
|
92
|
+
await storage.withLock(async () => {
|
|
93
|
+
const routines = await storage.readStore('routines');
|
|
94
|
+
routines.push(routine);
|
|
95
|
+
await storage.writeStore('routines', routines);
|
|
96
|
+
await storage.appendAudit(
|
|
97
|
+
buildAuditEvent({
|
|
98
|
+
tool: TOOL_NAMES.CREATE,
|
|
99
|
+
op: 'create',
|
|
100
|
+
context,
|
|
101
|
+
ids: [routine.id],
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return success({ routine: routine as unknown as Record<string, unknown> });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// routine:list
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
async function routineListTool(input: unknown, _context?: ToolContextLike): Promise<ToolOutput> {
|
|
114
|
+
const parsed = toRecord(input);
|
|
115
|
+
const limit = asInteger(parsed.limit);
|
|
116
|
+
|
|
117
|
+
const storage = getStoragePort();
|
|
118
|
+
const routines = await storage.readStore('routines');
|
|
119
|
+
|
|
120
|
+
const sorted = routines.toSorted((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
|
|
121
|
+
|
|
122
|
+
const items = limit && limit > 0 ? sorted.slice(0, limit) : sorted;
|
|
123
|
+
|
|
124
|
+
return success({
|
|
125
|
+
items: items as unknown as Record<string, unknown>,
|
|
126
|
+
count: items.length,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// routine:run (PLAN-ONLY -- does NOT execute tool steps)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async function routineRunTool(input: unknown, context?: ToolContextLike): Promise<ToolOutput> {
|
|
135
|
+
const parsed = toRecord(input);
|
|
136
|
+
const id = asNonEmptyString(parsed.id);
|
|
137
|
+
|
|
138
|
+
if (!id) {
|
|
139
|
+
return failure('INVALID_INPUT', 'id is required.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const storage = getStoragePort();
|
|
143
|
+
const routines = await storage.readStore('routines');
|
|
144
|
+
const routine = routines.find((r) => r.id === id);
|
|
145
|
+
|
|
146
|
+
if (!routine) {
|
|
147
|
+
return failure('NOT_FOUND', `routine ${id} was not found.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await storage.appendAudit(
|
|
151
|
+
buildAuditEvent({
|
|
152
|
+
tool: TOOL_NAMES.RUN,
|
|
153
|
+
op: 'execute',
|
|
154
|
+
context,
|
|
155
|
+
ids: [id],
|
|
156
|
+
details: { plan_only: true },
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return success({
|
|
161
|
+
routine_id: routine.id,
|
|
162
|
+
name: routine.name,
|
|
163
|
+
plan_only: true,
|
|
164
|
+
plan: routine.steps.map((step, index) => ({
|
|
165
|
+
index,
|
|
166
|
+
tool: step.tool,
|
|
167
|
+
input: step.input,
|
|
168
|
+
})),
|
|
169
|
+
governance: {
|
|
170
|
+
dispatch_required: true,
|
|
171
|
+
execution: 'No tool steps were executed by routine:run. This endpoint only returns a plan.',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Router (default export)
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
export default async function routineTools(
|
|
181
|
+
input: unknown,
|
|
182
|
+
context?: ToolContextLike,
|
|
183
|
+
): Promise<ToolOutput> {
|
|
184
|
+
switch (context?.tool_name) {
|
|
185
|
+
case TOOL_NAMES.CREATE:
|
|
186
|
+
return routineCreateTool(input, context);
|
|
187
|
+
case TOOL_NAMES.LIST:
|
|
188
|
+
return routineListTool(input, context);
|
|
189
|
+
case TOOL_NAMES.RUN:
|
|
190
|
+
return routineRunTool(input, context);
|
|
191
|
+
default:
|
|
192
|
+
return failure('UNKNOWN_TOOL', `Unknown routine tool: ${context?.tool_name ?? 'unknown'}`);
|
|
193
|
+
}
|
|
194
|
+
}
|