ticlawk 0.1.16-dev.1 → 0.1.16-dev.10
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/README.md +13 -0
- package/bin/ticlawk.mjs +116 -0
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +226 -28
- package/src/adapters/ticlawk/index.mjs +258 -113
- package/src/cli/agent-commands.mjs +594 -8
- package/src/core/agent-cli-handlers.mjs +443 -3
- package/src/core/agent-home.mjs +85 -0
- package/src/core/http.mjs +121 -0
- package/src/core/reminder-ticker.mjs +70 -0
- package/src/core/runtime-contract.mjs +1 -1
- package/src/core/runtime-support.mjs +31 -59
- package/src/core/ticlawk-control.mjs +3 -3
- package/src/migrate/write-initial-memory.mjs +101 -0
- package/src/runtimes/_shared/standing-prompt.mjs +296 -77
- package/src/runtimes/claude-code/index.mjs +28 -131
- package/src/runtimes/codex/index.mjs +15 -39
- package/src/runtimes/openclaw/index.mjs +39 -30
- package/src/runtimes/openclaw/target.mjs +0 -30
- package/src/runtimes/opencode/index.mjs +19 -54
- package/src/runtimes/pi/index.mjs +16 -49
- package/ticlawk.mjs +31 -6
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
package/src/core/http.mjs
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import {
|
|
3
|
+
handleAttachmentUpload,
|
|
4
|
+
handleAttachmentView,
|
|
5
|
+
handleGroupCreate,
|
|
3
6
|
handleGroupMembers,
|
|
7
|
+
handleGroupMembersAdd,
|
|
8
|
+
handleGroupMembersRemove,
|
|
9
|
+
handleMessageCheck,
|
|
10
|
+
handleMessageReact,
|
|
4
11
|
handleMessageRead,
|
|
12
|
+
handleMessageSearch,
|
|
5
13
|
handleMessageSend,
|
|
14
|
+
handleProfileAvatarUpload,
|
|
15
|
+
handleProfileShow,
|
|
16
|
+
handleProfileUpdate,
|
|
17
|
+
handleReminderCancel,
|
|
18
|
+
handleReminderList,
|
|
19
|
+
handleReminderLog,
|
|
20
|
+
handleReminderSchedule,
|
|
21
|
+
handleReminderSnooze,
|
|
22
|
+
handleReminderUpdate,
|
|
6
23
|
handleServerInfo,
|
|
7
24
|
handleTaskClaim,
|
|
25
|
+
handleTaskCreate,
|
|
8
26
|
handleTaskList,
|
|
27
|
+
handleTaskUnclaim,
|
|
9
28
|
handleTaskUpdate,
|
|
10
29
|
} from './agent-cli-handlers.mjs';
|
|
11
30
|
|
|
@@ -78,12 +97,24 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
78
97
|
const r = await handleMessageRead(req, parseQuery(req.url || ''), cliCtx);
|
|
79
98
|
return writeJson(res, r.status, r.body);
|
|
80
99
|
}
|
|
100
|
+
if (urlNoQuery === '/agent/task/create' && method === 'POST') {
|
|
101
|
+
const body = await readJsonBody(req);
|
|
102
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
103
|
+
const r = await handleTaskCreate(req, body, cliCtx);
|
|
104
|
+
return writeJson(res, r.status, r.body);
|
|
105
|
+
}
|
|
81
106
|
if (urlNoQuery === '/agent/task/claim' && method === 'POST') {
|
|
82
107
|
const body = await readJsonBody(req);
|
|
83
108
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
84
109
|
const r = await handleTaskClaim(req, body, cliCtx);
|
|
85
110
|
return writeJson(res, r.status, r.body);
|
|
86
111
|
}
|
|
112
|
+
if (urlNoQuery === '/agent/task/unclaim' && method === 'POST') {
|
|
113
|
+
const body = await readJsonBody(req);
|
|
114
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
115
|
+
const r = await handleTaskUnclaim(req, body, cliCtx);
|
|
116
|
+
return writeJson(res, r.status, r.body);
|
|
117
|
+
}
|
|
87
118
|
if (urlNoQuery === '/agent/task/update' && method === 'POST') {
|
|
88
119
|
const body = await readJsonBody(req);
|
|
89
120
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
@@ -94,10 +125,100 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
94
125
|
const r = await handleTaskList(req, parseQuery(req.url || ''), cliCtx);
|
|
95
126
|
return writeJson(res, r.status, r.body);
|
|
96
127
|
}
|
|
128
|
+
if (urlNoQuery === '/agent/message/react' && method === 'POST') {
|
|
129
|
+
const body = await readJsonBody(req);
|
|
130
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
131
|
+
const r = await handleMessageReact(req, body, cliCtx);
|
|
132
|
+
return writeJson(res, r.status, r.body);
|
|
133
|
+
}
|
|
134
|
+
if (urlNoQuery === '/agent/message/check' && method === 'GET') {
|
|
135
|
+
const r = await handleMessageCheck(req, parseQuery(req.url || ''), cliCtx);
|
|
136
|
+
return writeJson(res, r.status, r.body);
|
|
137
|
+
}
|
|
138
|
+
if (urlNoQuery === '/agent/message/search' && method === 'GET') {
|
|
139
|
+
const r = await handleMessageSearch(req, parseQuery(req.url || ''), cliCtx);
|
|
140
|
+
return writeJson(res, r.status, r.body);
|
|
141
|
+
}
|
|
142
|
+
if (urlNoQuery === '/agent/profile/show' && method === 'GET') {
|
|
143
|
+
const r = await handleProfileShow(req, parseQuery(req.url || ''), cliCtx);
|
|
144
|
+
return writeJson(res, r.status, r.body);
|
|
145
|
+
}
|
|
146
|
+
if (urlNoQuery === '/agent/profile/update' && method === 'POST') {
|
|
147
|
+
const body = await readJsonBody(req);
|
|
148
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
149
|
+
const r = await handleProfileUpdate(req, body, cliCtx);
|
|
150
|
+
return writeJson(res, r.status, r.body);
|
|
151
|
+
}
|
|
152
|
+
if (urlNoQuery === '/agent/profile/avatar' && method === 'POST') {
|
|
153
|
+
const body = await readJsonBody(req);
|
|
154
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
155
|
+
const r = await handleProfileAvatarUpload(req, body, cliCtx);
|
|
156
|
+
return writeJson(res, r.status, r.body);
|
|
157
|
+
}
|
|
158
|
+
if (urlNoQuery === '/agent/attachment/upload' && method === 'POST') {
|
|
159
|
+
const body = await readJsonBody(req);
|
|
160
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
161
|
+
const r = await handleAttachmentUpload(req, body, cliCtx);
|
|
162
|
+
return writeJson(res, r.status, r.body);
|
|
163
|
+
}
|
|
164
|
+
if (urlNoQuery === '/agent/attachment/view' && method === 'GET') {
|
|
165
|
+
const r = await handleAttachmentView(req, parseQuery(req.url || ''), cliCtx);
|
|
166
|
+
return writeJson(res, r.status, r.body);
|
|
167
|
+
}
|
|
168
|
+
if (urlNoQuery === '/agent/reminder/schedule' && method === 'POST') {
|
|
169
|
+
const body = await readJsonBody(req);
|
|
170
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
171
|
+
const r = await handleReminderSchedule(req, body, cliCtx);
|
|
172
|
+
return writeJson(res, r.status, r.body);
|
|
173
|
+
}
|
|
174
|
+
if (urlNoQuery === '/agent/reminder/list' && method === 'GET') {
|
|
175
|
+
const r = await handleReminderList(req, parseQuery(req.url || ''), cliCtx);
|
|
176
|
+
return writeJson(res, r.status, r.body);
|
|
177
|
+
}
|
|
178
|
+
if (urlNoQuery === '/agent/reminder/snooze' && method === 'POST') {
|
|
179
|
+
const body = await readJsonBody(req);
|
|
180
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
181
|
+
const r = await handleReminderSnooze(req, body, cliCtx);
|
|
182
|
+
return writeJson(res, r.status, r.body);
|
|
183
|
+
}
|
|
184
|
+
if (urlNoQuery === '/agent/reminder/update' && method === 'POST') {
|
|
185
|
+
const body = await readJsonBody(req);
|
|
186
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
187
|
+
const r = await handleReminderUpdate(req, body, cliCtx);
|
|
188
|
+
return writeJson(res, r.status, r.body);
|
|
189
|
+
}
|
|
190
|
+
if (urlNoQuery === '/agent/reminder/cancel' && method === 'POST') {
|
|
191
|
+
const body = await readJsonBody(req);
|
|
192
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
193
|
+
const r = await handleReminderCancel(req, body, cliCtx);
|
|
194
|
+
return writeJson(res, r.status, r.body);
|
|
195
|
+
}
|
|
196
|
+
if (urlNoQuery === '/agent/reminder/log' && method === 'GET') {
|
|
197
|
+
const r = await handleReminderLog(req, parseQuery(req.url || ''), cliCtx);
|
|
198
|
+
return writeJson(res, r.status, r.body);
|
|
199
|
+
}
|
|
97
200
|
if (urlNoQuery === '/agent/group/members' && method === 'GET') {
|
|
98
201
|
const r = await handleGroupMembers(req, parseQuery(req.url || ''), cliCtx);
|
|
99
202
|
return writeJson(res, r.status, r.body);
|
|
100
203
|
}
|
|
204
|
+
if (urlNoQuery === '/agent/group/create' && method === 'POST') {
|
|
205
|
+
const body = await readJsonBody(req);
|
|
206
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
207
|
+
const r = await handleGroupCreate(req, body, cliCtx);
|
|
208
|
+
return writeJson(res, r.status, r.body);
|
|
209
|
+
}
|
|
210
|
+
if (urlNoQuery === '/agent/group/members/add' && method === 'POST') {
|
|
211
|
+
const body = await readJsonBody(req);
|
|
212
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
213
|
+
const r = await handleGroupMembersAdd(req, body, cliCtx);
|
|
214
|
+
return writeJson(res, r.status, r.body);
|
|
215
|
+
}
|
|
216
|
+
if (urlNoQuery === '/agent/group/members/remove' && method === 'POST') {
|
|
217
|
+
const body = await readJsonBody(req);
|
|
218
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
219
|
+
const r = await handleGroupMembersRemove(req, body, cliCtx);
|
|
220
|
+
return writeJson(res, r.status, r.body);
|
|
221
|
+
}
|
|
101
222
|
if (urlNoQuery === '/agent/server/info' && method === 'GET') {
|
|
102
223
|
const r = await handleServerInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
103
224
|
return writeJson(res, r.status, r.body);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-side reminder ticker.
|
|
3
|
+
*
|
|
4
|
+
* Runs every 60s. POSTs /api/agent/reminders/fire-due via the existing
|
|
5
|
+
* service-role connector path. The edge function calls the
|
|
6
|
+
* fire_due_reminders SQL function which atomically claims due
|
|
7
|
+
* reminders, posts a system message into each anchor conversation, and
|
|
8
|
+
* inserts the explicit message_delivery row to the owner agent.
|
|
9
|
+
*
|
|
10
|
+
* Failures are logged at boundaries and do not retry — the next tick
|
|
11
|
+
* picks them up. This is the only writer of "reminder fired" state, so
|
|
12
|
+
* there is no double-fire risk even across multiple daemon restarts
|
|
13
|
+
* (FOR UPDATE SKIP LOCKED inside the SQL function serialises).
|
|
14
|
+
*
|
|
15
|
+
* Disabled by setting TICLAWK_DISABLE_REMINDER_TICKER=1 in the
|
|
16
|
+
* environment (useful if you want to drive firing from elsewhere).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { fireDueReminders } from '../adapters/ticlawk/api.mjs';
|
|
20
|
+
import { debugError, debugLog } from './logger.mjs';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_INTERVAL_MS = 60 * 1000;
|
|
23
|
+
|
|
24
|
+
export function startReminderTicker({
|
|
25
|
+
intervalMs = DEFAULT_INTERVAL_MS,
|
|
26
|
+
} = {}) {
|
|
27
|
+
if (String(process.env.TICLAWK_DISABLE_REMINDER_TICKER || '').trim() === '1') {
|
|
28
|
+
debugLog('reminder-ticker', 'disabled', { reason: 'TICLAWK_DISABLE_REMINDER_TICKER=1' });
|
|
29
|
+
return { stop: () => {} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let stopped = false;
|
|
33
|
+
let timer = null;
|
|
34
|
+
|
|
35
|
+
async function tick() {
|
|
36
|
+
if (stopped) return;
|
|
37
|
+
try {
|
|
38
|
+
const res = await fireDueReminders();
|
|
39
|
+
const count = res?.fired_count || 0;
|
|
40
|
+
if (count > 0) {
|
|
41
|
+
debugLog('reminder-ticker', 'fired', {
|
|
42
|
+
count,
|
|
43
|
+
ids: (res?.fired || []).map((r) => r?.reminder_id),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
debugError('reminder-ticker', 'tick.failed', {
|
|
48
|
+
error: err?.message || String(err),
|
|
49
|
+
status: err?.status || null,
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
if (!stopped) timer = setTimeout(tick, intervalMs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
debugLog('reminder-ticker', 'start', { intervalMs });
|
|
57
|
+
// Start with an initial small delay so the daemon's HTTP server is
|
|
58
|
+
// up before the first tick.
|
|
59
|
+
timer = setTimeout(tick, 5000);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
stop() {
|
|
63
|
+
stopped = true;
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
timer = null;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -42,7 +42,7 @@ import { normalizeServiceType } from './runtime-registry.mjs';
|
|
|
42
42
|
* @property {(bindingId: string) => any} getBinding
|
|
43
43
|
* @property {(filters?: Record<string, any>) => any[]} listBindings
|
|
44
44
|
* @property {(bindingId: string) => Promise<void>} [deleteBinding]
|
|
45
|
-
* @property {(binding: any) => Promise<any>} [
|
|
45
|
+
* @property {(binding: any) => Promise<any>} [persistBinding]
|
|
46
46
|
* @property {(binding: any) => Promise<any>} upsertBinding
|
|
47
47
|
* @property {(inbound: any, runtimeName: string) => Promise<string>} buildImageMessageFromInbound
|
|
48
48
|
* @property {any} logger
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getStreamingMode } from './config.mjs';
|
|
2
|
+
import { getAgentHome } from './agent-home.mjs';
|
|
2
3
|
const ERROR_MAX_CHARS = 500;
|
|
3
4
|
const DEFAULT_DELTA_FLUSH_MS = 250;
|
|
4
5
|
const DEFAULT_DELTA_FLUSH_CHARS = 64;
|
|
@@ -15,7 +16,7 @@ function runtimeContextLines(binding, info = {}) {
|
|
|
15
16
|
const lines = [];
|
|
16
17
|
if (meta.sessionId) lines.push(`Session: ${meta.sessionId}`);
|
|
17
18
|
if (info.turnId) lines.push(`Turn: ${info.turnId}`);
|
|
18
|
-
if (
|
|
19
|
+
if (binding?.id) lines.push(`Workdir: ${getAgentHome(binding.id)}`);
|
|
19
20
|
return lines;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -41,72 +42,43 @@ export function shouldStreamRuntime(runtimeName, runtime) {
|
|
|
41
42
|
return Boolean(runtime?.runTurnStream) && getStreamingMode(runtimeName);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export async function sendAdapterMessage(adapter, binding, payload) {
|
|
45
|
-
await adapter.send(binding, payload);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Record the runtime's final turn output as activity (NOT chat).
|
|
50
|
-
*
|
|
51
|
-
* Previously called `sendResult` and treated as "the chat reply path".
|
|
52
|
-
* After the group-chat upgrade, chat is produced exclusively by the
|
|
53
|
-
* agent invoking `ticlawk message send` via the CLI. The runtime's
|
|
54
|
-
* raw final output is still surfaced for trajectory/debug UI, but it
|
|
55
|
-
* no longer materializes as a `messages` row — the trigger
|
|
56
|
-
* `project_agent_event` was updated in PR-2b to drop the chat
|
|
57
|
-
* projection.
|
|
58
|
-
*
|
|
59
|
-
* Renamed so the call sites read self-evidently: "record activity"
|
|
60
|
-
* never reads as "send a chat message".
|
|
61
|
-
*/
|
|
62
|
-
export async function recordActivity(adapter, binding, inbound, result) {
|
|
63
|
-
if (!result?.text && (!result?.mediaUrls || result.mediaUrls.length === 0)) return;
|
|
64
|
-
await sendAdapterMessage(adapter, binding, {
|
|
65
|
-
type: 'assistant',
|
|
66
|
-
text: result.text || '',
|
|
67
|
-
media: result.media || [],
|
|
68
|
-
turnId: inbound.messageId || result?.turnId || null,
|
|
69
|
-
replyToMessageId: inbound.messageId || null,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
45
|
export async function reportSubprocessFailure({ adapter, binding, inbound, runtimeName, info }) {
|
|
75
46
|
if (!info || info.ok) return;
|
|
76
47
|
const seconds = ((info.durationMs || 0) / 1000).toFixed(1);
|
|
77
|
-
if (runtimeName === 'Codex' && isRuntimeGatewayFailure(info)) {
|
|
78
|
-
await sendAdapterMessage(adapter, binding, {
|
|
79
|
-
type: 'assistant',
|
|
80
|
-
text: buildCodexGatewayFailureText({ binding, info, seconds }),
|
|
81
|
-
media: [],
|
|
82
|
-
turnId: inbound?.messageId || null,
|
|
83
|
-
replyToMessageId: inbound?.messageId || null,
|
|
84
|
-
});
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
48
|
|
|
88
|
-
let
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
} else if (info.kind === 'spawn-failed') {
|
|
92
|
-
summary = `⚠️ ${runtimeName} failed to spawn: ${info.error || 'unknown error'}.`;
|
|
93
|
-
} else if (info.kind === 'gateway-timeout') {
|
|
94
|
-
summary = `⚠️ ${runtimeName} did not respond after ${seconds}s (timeout).`;
|
|
95
|
-
} else if (info.kind === 'gateway-error') {
|
|
96
|
-
summary = `⚠️ ${runtimeName} returned an error after ${seconds}s.`;
|
|
97
|
-
} else if (typeof info.code === 'number') {
|
|
98
|
-
summary = `⚠️ ${runtimeName} exited with code ${info.code} after ${seconds}s.`;
|
|
49
|
+
let text;
|
|
50
|
+
if (runtimeName === 'Codex' && isRuntimeGatewayFailure(info)) {
|
|
51
|
+
text = buildCodexGatewayFailureText({ binding, info, seconds });
|
|
99
52
|
} else {
|
|
100
|
-
summary
|
|
53
|
+
let summary;
|
|
54
|
+
if (info.kind === 'killed') {
|
|
55
|
+
summary = `⚠️ ${runtimeName} was killed by signal ${info.signal} after ${seconds}s.`;
|
|
56
|
+
} else if (info.kind === 'spawn-failed') {
|
|
57
|
+
summary = `⚠️ ${runtimeName} failed to spawn: ${info.error || 'unknown error'}.`;
|
|
58
|
+
} else if (info.kind === 'gateway-timeout') {
|
|
59
|
+
summary = `⚠️ ${runtimeName} did not respond after ${seconds}s (timeout).`;
|
|
60
|
+
} else if (info.kind === 'gateway-error') {
|
|
61
|
+
summary = `⚠️ ${runtimeName} returned an error after ${seconds}s.`;
|
|
62
|
+
} else if (typeof info.code === 'number') {
|
|
63
|
+
summary = `⚠️ ${runtimeName} exited with code ${info.code} after ${seconds}s.`;
|
|
64
|
+
} else {
|
|
65
|
+
summary = `⚠️ ${runtimeName} failed after ${seconds}s.`;
|
|
66
|
+
}
|
|
67
|
+
const detail = truncateError(info.errorMessage);
|
|
68
|
+
text = detail ? `${summary}\n\n${detail}` : `${summary}\n\nCheck ~/.ticlawk/ticlawk.log on the host for details.`;
|
|
101
69
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
70
|
+
|
|
71
|
+
// Runtime never got to invoke `ticlawk message send`, so the daemon
|
|
72
|
+
// speaks for the agent. visibility='admin' so the message reaches
|
|
73
|
+
// owner/admin members only — the enqueue_message_deliveries trigger
|
|
74
|
+
// skips fan-out to member-role agents, breaking what would otherwise
|
|
75
|
+
// be a failure→fan-out→failure cascade when several agents share a
|
|
76
|
+
// broken runtime.
|
|
77
|
+
await adapter.postAgentReply(binding, {
|
|
78
|
+
conversationId: inbound?.conversationId || null,
|
|
106
79
|
text,
|
|
107
|
-
media: [],
|
|
108
|
-
turnId: inbound?.messageId || null,
|
|
109
80
|
replyToMessageId: inbound?.messageId || null,
|
|
81
|
+
visibility: 'admin',
|
|
110
82
|
});
|
|
111
83
|
}
|
|
112
84
|
|
|
@@ -30,7 +30,7 @@ function createUpsertBindingWithSync(runtimes, adapter) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function createPersistBinding(runtimes, getAdapter) {
|
|
34
34
|
return async (binding) => {
|
|
35
35
|
const nextBinding = await upsertBinding(binding);
|
|
36
36
|
const runtime = nextBinding?.runtime ? runtimes[nextBinding.runtime] : null;
|
|
@@ -48,7 +48,7 @@ export async function createTiclawkController(adapterId = ADAPTER_ID) {
|
|
|
48
48
|
const { runtimes } = await buildRuntimeContext();
|
|
49
49
|
const resolveRuntimeBinding = createResolveRuntimeBinding(runtimes);
|
|
50
50
|
let adapter;
|
|
51
|
-
const
|
|
51
|
+
const persistBinding = createPersistBinding(runtimes, () => adapter);
|
|
52
52
|
let syncBinding = async (binding) => {
|
|
53
53
|
if (!adapter) {
|
|
54
54
|
throw new Error('adapter not initialized');
|
|
@@ -64,7 +64,7 @@ export async function createTiclawkController(adapterId = ADAPTER_ID) {
|
|
|
64
64
|
deleteBinding,
|
|
65
65
|
findBindingByTarget,
|
|
66
66
|
resolveRuntimeBinding,
|
|
67
|
-
|
|
67
|
+
persistBinding: (binding) => persistBinding(binding),
|
|
68
68
|
upsertBinding: (binding) => syncBinding(binding),
|
|
69
69
|
buildImageMessageFromInbound,
|
|
70
70
|
logger,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Seed the per-agent home + MEMORY.md for every paired agent in the
|
|
3
|
+
// linked Supabase project.
|
|
4
|
+
//
|
|
5
|
+
// ~/.ticlawk/agents/<agent_id>/MEMORY.md
|
|
6
|
+
//
|
|
7
|
+
// Replaces the Phase-B variant that wrote MEMORY.md into each agent's
|
|
8
|
+
// project workdir. Each agent now has its own home dir as cwd, with
|
|
9
|
+
// MEMORY.md living at the root of that home.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node src/migrate/write-initial-memory.mjs # dry-run
|
|
13
|
+
// node src/migrate/write-initial-memory.mjs --apply # actually write
|
|
14
|
+
//
|
|
15
|
+
// Idempotent: existing MEMORY.md files are never overwritten.
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import { ensureAgentHome, getAgentHome, getAgentMemoryPath } from '../core/agent-home.mjs';
|
|
19
|
+
|
|
20
|
+
const APPLY = process.argv.includes('--apply');
|
|
21
|
+
|
|
22
|
+
function loadEnv(file) {
|
|
23
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const line of text.split('\n')) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
28
|
+
const eq = trimmed.indexOf('=');
|
|
29
|
+
if (eq < 0) continue;
|
|
30
|
+
const k = trimmed.slice(0, eq).trim();
|
|
31
|
+
let v = trimmed.slice(eq + 1).trim();
|
|
32
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
33
|
+
v = v.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
out[k] = v;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ENV_PATH = '/home/wei/Projects/ticlawk/.env.local';
|
|
41
|
+
const env = loadEnv(ENV_PATH);
|
|
42
|
+
const SUPABASE_URL = env.SUPABASE_URL;
|
|
43
|
+
const SUPABASE_SECRET_KEY = env.SUPABASE_SECRET_KEY;
|
|
44
|
+
if (!SUPABASE_URL || !SUPABASE_SECRET_KEY) {
|
|
45
|
+
console.error('Missing SUPABASE_URL or SUPABASE_SECRET_KEY in', ENV_PATH);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function rest(pathRel, init = {}) {
|
|
50
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/${pathRel}`, {
|
|
51
|
+
...init,
|
|
52
|
+
headers: {
|
|
53
|
+
apikey: SUPABASE_SECRET_KEY,
|
|
54
|
+
Authorization: `Bearer ${SUPABASE_SECRET_KEY}`,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
...(init.headers || {}),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = await res.text();
|
|
61
|
+
throw new Error(`REST ${pathRel} → ${res.status}: ${body}`);
|
|
62
|
+
}
|
|
63
|
+
return res.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
const agents = await rest('agents?select=id,name,display_name,service_type,meta');
|
|
68
|
+
const plan = agents.map((agent) => ({
|
|
69
|
+
agent,
|
|
70
|
+
home: getAgentHome(agent.id),
|
|
71
|
+
memoryPath: getAgentMemoryPath(agent.id),
|
|
72
|
+
already: fs.existsSync(getAgentMemoryPath(agent.id)),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
console.log(`[memory-migrate] ${plan.length} agents; mode=${APPLY ? 'apply' : 'dry-run'}`);
|
|
76
|
+
for (const p of plan) {
|
|
77
|
+
const tag = p.already ? 'SKIP ' : 'WRITE';
|
|
78
|
+
const note = p.already ? 'MEMORY.md already present' : `→ ${p.memoryPath}`;
|
|
79
|
+
console.log(`[memory-migrate] ${tag} ${p.agent.name || p.agent.id}: ${note}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!APPLY) {
|
|
83
|
+
console.log('[memory-migrate] dry-run done. Re-run with --apply to write files.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let written = 0;
|
|
88
|
+
for (const p of plan) {
|
|
89
|
+
if (p.already) continue;
|
|
90
|
+
ensureAgentHome(p.agent.id, {
|
|
91
|
+
displayName: p.agent.display_name || p.agent.name || null,
|
|
92
|
+
});
|
|
93
|
+
written += 1;
|
|
94
|
+
}
|
|
95
|
+
console.log(`[memory-migrate] wrote ${written} MEMORY.md files (skipped ${plan.length - written}).`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((err) => {
|
|
99
|
+
console.error('[memory-migrate] failed:', err.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|