ticlawk 0.1.15-dev.6 → 0.1.16-dev.1
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 +83 -212
- package/bin/ticlawk.mjs +107 -46
- package/package.json +2 -5
- package/src/adapters/ticlawk/api.mjs +93 -26
- package/src/adapters/ticlawk/credentials.mjs +1 -2
- package/src/adapters/ticlawk/index.mjs +78 -32
- package/src/cli/agent-commands.mjs +290 -0
- package/src/core/adapter-registry.mjs +12 -28
- package/src/core/agent-cli-handlers.mjs +291 -0
- package/src/core/config.mjs +0 -15
- package/src/core/http.mjs +90 -18
- package/src/core/runtime-env.mjs +41 -5
- package/src/core/runtime-support.mjs +16 -1
- package/src/core/ticlawk-control.mjs +4 -3
- package/src/runtimes/_shared/standing-prompt.mjs +89 -0
- package/src/runtimes/claude-code/index.mjs +26 -7
- package/src/runtimes/claude-code/session.mjs +15 -7
- package/src/runtimes/codex/index.mjs +18 -6
- package/src/runtimes/codex/session.mjs +9 -5
- package/src/runtimes/openclaw/index.mjs +22 -3
- package/src/runtimes/opencode/index.mjs +19 -6
- package/src/runtimes/opencode/session.mjs +11 -2
- package/src/runtimes/pi/index.mjs +19 -6
- package/src/runtimes/pi/session.mjs +8 -2
- package/ticlawk.mjs +6 -4
- package/assets/ticlawk-concept.svg +0 -137
- package/src/adapters/telegram/index.mjs +0 -359
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-facing CLI subcommands.
|
|
3
|
+
*
|
|
4
|
+
* These run inside a runtime (Codex / Claude Code / etc.) and post to
|
|
5
|
+
* the local daemon at TICLAWK_RUNTIME_DAEMON_URL (default
|
|
6
|
+
* http://127.0.0.1:8741). The daemon validates the caller's
|
|
7
|
+
* TICLAWK_RUNTIME_AGENT_ID against active bindings before forwarding to
|
|
8
|
+
* the ticlawk backend.
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* ticlawk message send --target <t> [--seen-up-to-seq N] [--reply-to <msg>]
|
|
12
|
+
* ticlawk message read --target <t> [--around <msg>] [--limit N]
|
|
13
|
+
* ticlawk task claim --message-id <id> [--lease-seconds N]
|
|
14
|
+
* ticlawk task update --task-id <id> --status <s>
|
|
15
|
+
* ticlawk task list [--target <t>]
|
|
16
|
+
* ticlawk group members --target <t>
|
|
17
|
+
* ticlawk server info [--refresh]
|
|
18
|
+
*
|
|
19
|
+
* `ticlawk message send` reads the message body from stdin so heredocs
|
|
20
|
+
* work cleanly (matching the Slock convention).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { request as httpRequest } from 'node:http';
|
|
24
|
+
import { request as httpsRequest } from 'node:https';
|
|
25
|
+
|
|
26
|
+
function getDaemonUrl() {
|
|
27
|
+
return process.env.TICLAWK_RUNTIME_DAEMON_URL || 'http://127.0.0.1:8741';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireAgentEnv() {
|
|
31
|
+
const agentId = String(process.env.TICLAWK_RUNTIME_AGENT_ID || '').trim();
|
|
32
|
+
if (!agentId) {
|
|
33
|
+
console.error('TICLAWK_RUNTIME_AGENT_ID is required (must be set by the daemon when spawning the runtime).');
|
|
34
|
+
console.error('If you are seeing this outside an agent runtime, this command is not for you.');
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
agentId,
|
|
39
|
+
hostId: String(process.env.TICLAWK_RUNTIME_HOST_ID || '').trim() || null,
|
|
40
|
+
sessionId: String(process.env.TICLAWK_RUNTIME_SESSION_ID || '').trim() || null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function commonHeaders(env) {
|
|
45
|
+
const headers = {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'X-Ticlawk-Acting-Agent-Id': env.agentId,
|
|
48
|
+
};
|
|
49
|
+
if (env.hostId) headers['X-Ticlawk-Runtime-Host-Id'] = env.hostId;
|
|
50
|
+
if (env.sessionId) headers['X-Ticlawk-Runtime-Session-Id'] = env.sessionId;
|
|
51
|
+
return headers;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function daemonRequest({ method, path, headers, body }) {
|
|
55
|
+
const url = new URL(path, getDaemonUrl());
|
|
56
|
+
const isHttps = url.protocol === 'https:';
|
|
57
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
58
|
+
const payload = body == null ? null : (typeof body === 'string' ? body : JSON.stringify(body));
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const req = requestFn({
|
|
61
|
+
hostname: url.hostname,
|
|
62
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
63
|
+
path: `${url.pathname}${url.search}`,
|
|
64
|
+
method,
|
|
65
|
+
headers: {
|
|
66
|
+
...headers,
|
|
67
|
+
...(payload != null ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
68
|
+
},
|
|
69
|
+
}, (res) => {
|
|
70
|
+
let text = '';
|
|
71
|
+
res.on('data', (chunk) => { text += chunk; });
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
let parsed = null;
|
|
74
|
+
if (text) {
|
|
75
|
+
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
|
76
|
+
}
|
|
77
|
+
resolve({ statusCode: res.statusCode || 0, body: parsed, raw: text });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
req.on('error', reject);
|
|
81
|
+
if (payload != null) req.write(payload);
|
|
82
|
+
req.end();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function readStdin() {
|
|
87
|
+
if (process.stdin.isTTY) return '';
|
|
88
|
+
let buf = '';
|
|
89
|
+
for await (const chunk of process.stdin) buf += chunk;
|
|
90
|
+
return buf;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getArg(args, name) {
|
|
94
|
+
return args[name] != null ? String(args[name]) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getNumberArg(args, name) {
|
|
98
|
+
const v = args[name];
|
|
99
|
+
if (v == null) return null;
|
|
100
|
+
const n = Number(v);
|
|
101
|
+
return Number.isFinite(n) ? n : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printJson(value) {
|
|
105
|
+
console.log(JSON.stringify(value, null, 2));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function exitFromStatus(statusCode, errorBody) {
|
|
109
|
+
if (statusCode >= 200 && statusCode < 300) return 0;
|
|
110
|
+
if (statusCode === 409) {
|
|
111
|
+
if (errorBody) printJson(errorBody);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
if (errorBody) printJson(errorBody);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function runMessageSendCommand(args) {
|
|
119
|
+
const env = requireAgentEnv();
|
|
120
|
+
const target = getArg(args, 'target');
|
|
121
|
+
const conversationId = getArg(args, 'conversation-id');
|
|
122
|
+
if (!target && !conversationId) {
|
|
123
|
+
console.error('--target or --conversation-id is required');
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
const text = await readStdin();
|
|
127
|
+
if (!text || !text.trim()) {
|
|
128
|
+
console.error('message body is required on stdin');
|
|
129
|
+
console.error('Example:');
|
|
130
|
+
console.error(" echo \"hello\" | ticlawk message send --target dm:@alice");
|
|
131
|
+
console.error(" ticlawk message send --target \"#frontend\" <<'EOF'");
|
|
132
|
+
console.error(' ...message...');
|
|
133
|
+
console.error(' EOF');
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
const body = {
|
|
137
|
+
target,
|
|
138
|
+
conversation_id: conversationId,
|
|
139
|
+
text: text.replace(/\n+$/, ''),
|
|
140
|
+
seen_up_to_seq: getNumberArg(args, 'seen-up-to-seq'),
|
|
141
|
+
reply_to_message_id: getArg(args, 'reply-to'),
|
|
142
|
+
};
|
|
143
|
+
const res = await daemonRequest({
|
|
144
|
+
method: 'POST',
|
|
145
|
+
path: '/agent/message/send',
|
|
146
|
+
headers: commonHeaders(env),
|
|
147
|
+
body,
|
|
148
|
+
});
|
|
149
|
+
printJson(res.body);
|
|
150
|
+
return exitFromStatus(res.statusCode, null);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function runMessageReadCommand(args) {
|
|
154
|
+
const env = requireAgentEnv();
|
|
155
|
+
const target = getArg(args, 'target');
|
|
156
|
+
const conversationId = getArg(args, 'conversation-id');
|
|
157
|
+
if (!target && !conversationId) {
|
|
158
|
+
console.error('--target or --conversation-id is required');
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
const params = new URLSearchParams();
|
|
162
|
+
if (target) params.set('target', target);
|
|
163
|
+
if (conversationId) params.set('conversation_id', conversationId);
|
|
164
|
+
const around = getArg(args, 'around');
|
|
165
|
+
if (around) params.set('around_message_id', around);
|
|
166
|
+
const beforeSeq = getArg(args, 'before-seq');
|
|
167
|
+
if (beforeSeq) params.set('before_seq', beforeSeq);
|
|
168
|
+
const limit = getArg(args, 'limit');
|
|
169
|
+
if (limit) params.set('limit', limit);
|
|
170
|
+
|
|
171
|
+
const res = await daemonRequest({
|
|
172
|
+
method: 'GET',
|
|
173
|
+
path: `/agent/message/read?${params}`,
|
|
174
|
+
headers: commonHeaders(env),
|
|
175
|
+
});
|
|
176
|
+
printJson(res.body);
|
|
177
|
+
return exitFromStatus(res.statusCode);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function runTaskClaimCommand(args) {
|
|
181
|
+
const env = requireAgentEnv();
|
|
182
|
+
const messageId = getArg(args, 'message-id');
|
|
183
|
+
if (!messageId) {
|
|
184
|
+
console.error('--message-id is required');
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
187
|
+
const res = await daemonRequest({
|
|
188
|
+
method: 'POST',
|
|
189
|
+
path: '/agent/task/claim',
|
|
190
|
+
headers: commonHeaders(env),
|
|
191
|
+
body: {
|
|
192
|
+
source_message_id: messageId,
|
|
193
|
+
lease_seconds: getNumberArg(args, 'lease-seconds'),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
printJson(res.body);
|
|
197
|
+
if (res.statusCode === 409) {
|
|
198
|
+
console.error(`task already claimed by another agent`);
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
return exitFromStatus(res.statusCode);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function runTaskUpdateCommand(args) {
|
|
205
|
+
const env = requireAgentEnv();
|
|
206
|
+
const taskId = getArg(args, 'task-id');
|
|
207
|
+
const status = getArg(args, 'status');
|
|
208
|
+
if (!taskId || !status) {
|
|
209
|
+
console.error('--task-id and --status are required');
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
const res = await daemonRequest({
|
|
213
|
+
method: 'POST',
|
|
214
|
+
path: '/agent/task/update',
|
|
215
|
+
headers: commonHeaders(env),
|
|
216
|
+
body: { task_id: taskId, status },
|
|
217
|
+
});
|
|
218
|
+
printJson(res.body);
|
|
219
|
+
return exitFromStatus(res.statusCode);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function runTaskListCommand(args) {
|
|
223
|
+
const env = requireAgentEnv();
|
|
224
|
+
const params = new URLSearchParams();
|
|
225
|
+
const target = getArg(args, 'target');
|
|
226
|
+
const conversationId = getArg(args, 'conversation-id');
|
|
227
|
+
if (target) params.set('target', target);
|
|
228
|
+
if (conversationId) params.set('conversation_id', conversationId);
|
|
229
|
+
const res = await daemonRequest({
|
|
230
|
+
method: 'GET',
|
|
231
|
+
path: `/agent/task/list?${params}`,
|
|
232
|
+
headers: commonHeaders(env),
|
|
233
|
+
});
|
|
234
|
+
printJson(res.body);
|
|
235
|
+
return exitFromStatus(res.statusCode);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function runGroupMembersCommand(args) {
|
|
239
|
+
const env = requireAgentEnv();
|
|
240
|
+
const target = getArg(args, 'target');
|
|
241
|
+
const conversationId = getArg(args, 'conversation-id');
|
|
242
|
+
if (!target && !conversationId) {
|
|
243
|
+
console.error('--target or --conversation-id is required');
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
const params = new URLSearchParams();
|
|
247
|
+
if (target) params.set('target', target);
|
|
248
|
+
if (conversationId) params.set('conversation_id', conversationId);
|
|
249
|
+
const res = await daemonRequest({
|
|
250
|
+
method: 'GET',
|
|
251
|
+
path: `/agent/group/members?${params}`,
|
|
252
|
+
headers: commonHeaders(env),
|
|
253
|
+
});
|
|
254
|
+
printJson(res.body);
|
|
255
|
+
return exitFromStatus(res.statusCode);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function runServerInfoCommand(args) {
|
|
259
|
+
const env = requireAgentEnv();
|
|
260
|
+
const params = new URLSearchParams();
|
|
261
|
+
if (args.refresh) params.set('refresh', '1');
|
|
262
|
+
const res = await daemonRequest({
|
|
263
|
+
method: 'GET',
|
|
264
|
+
path: `/agent/server/info?${params}`,
|
|
265
|
+
headers: commonHeaders(env),
|
|
266
|
+
});
|
|
267
|
+
printJson(res.body);
|
|
268
|
+
return exitFromStatus(res.statusCode);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const AGENT_COMMAND_HELP = {
|
|
272
|
+
message: `ticlawk message <send|read>
|
|
273
|
+
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>]
|
|
274
|
+
Body is read from stdin (use <<'EOF' ... EOF for multiline).
|
|
275
|
+
Targets:
|
|
276
|
+
dm:@<user> private message
|
|
277
|
+
#<group> group conversation
|
|
278
|
+
#<group>:<msgid> thread under a top-level message in that group
|
|
279
|
+
ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
|
|
280
|
+
`,
|
|
281
|
+
task: `ticlawk task <claim|update|list>
|
|
282
|
+
ticlawk task claim --message-id <id> [--lease-seconds N]
|
|
283
|
+
ticlawk task update --task-id <id> --status <open|claimed|done|canceled>
|
|
284
|
+
ticlawk task list [--target <target>]
|
|
285
|
+
`,
|
|
286
|
+
group: `ticlawk group members --target "<target>"
|
|
287
|
+
`,
|
|
288
|
+
server: `ticlawk server info [--refresh]
|
|
289
|
+
`,
|
|
290
|
+
};
|
|
@@ -3,48 +3,32 @@ import {
|
|
|
3
3
|
getTiclawkAuthHelp,
|
|
4
4
|
runTiclawkAuth,
|
|
5
5
|
} from '../adapters/ticlawk/index.mjs';
|
|
6
|
-
import {
|
|
7
|
-
createTelegramAdapter,
|
|
8
|
-
getTelegramAuthHelp,
|
|
9
|
-
runTelegramAuth,
|
|
10
|
-
} from '../adapters/telegram/index.mjs';
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Ticlawk has a single adapter: ticlawk itself. The registry shape is kept
|
|
9
|
+
* so callers can still depend on a stable surface (createAdapter, etc.)
|
|
10
|
+
* without branching on adapter ids.
|
|
11
|
+
*/
|
|
16
12
|
|
|
17
|
-
const
|
|
18
|
-
ticlawk: {
|
|
19
|
-
help: getTiclawkAuthHelp,
|
|
20
|
-
run: runTiclawkAuth,
|
|
21
|
-
},
|
|
22
|
-
telegram: {
|
|
23
|
-
help: getTelegramAuthHelp,
|
|
24
|
-
run: runTelegramAuth,
|
|
25
|
-
},
|
|
26
|
-
};
|
|
13
|
+
const ADAPTER_ID = 'ticlawk';
|
|
27
14
|
|
|
28
15
|
export function createAdapter(adapterId, ctx) {
|
|
29
|
-
|
|
30
|
-
if (!factory) {
|
|
16
|
+
if (adapterId !== ADAPTER_ID) {
|
|
31
17
|
throw new Error(`unsupported adapter: ${adapterId}`);
|
|
32
18
|
}
|
|
33
|
-
return
|
|
19
|
+
return createTiclawkAdapter(ctx);
|
|
34
20
|
}
|
|
35
21
|
|
|
36
22
|
export function getAdapterAuthHelp(adapterId) {
|
|
37
|
-
|
|
38
|
-
if (!handler?.help) {
|
|
23
|
+
if (adapterId !== ADAPTER_ID) {
|
|
39
24
|
throw new Error(`unsupported adapter: ${adapterId}`);
|
|
40
25
|
}
|
|
41
|
-
return
|
|
26
|
+
return getTiclawkAuthHelp();
|
|
42
27
|
}
|
|
43
28
|
|
|
44
29
|
export async function runAdapterAuth(adapterId, rawArgs) {
|
|
45
|
-
|
|
46
|
-
if (!handler?.run) {
|
|
30
|
+
if (adapterId !== ADAPTER_ID) {
|
|
47
31
|
throw new Error(`unsupported adapter: ${adapterId}`);
|
|
48
32
|
}
|
|
49
|
-
return
|
|
33
|
+
return runTiclawkAuth(rawArgs);
|
|
50
34
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-side handlers for the agent CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* The CLI subcommands (`ticlawk message send`, `ticlawk task claim`, ...)
|
|
5
|
+
* post to the local daemon at 127.0.0.1:8741. The daemon validates the
|
|
6
|
+
* caller's TICLAWK_RUNTIME_AGENT_ID against the active binding store
|
|
7
|
+
* and forwards to the ticlawk backend using the connector API key.
|
|
8
|
+
*
|
|
9
|
+
* Targets are parsed in the daemon (not on the wire to backend) so the
|
|
10
|
+
* CLI can speak `#<group>` / `dm:@<user>` / `#<group>:<short-msg-id>`
|
|
11
|
+
* while backend keeps a flat conversation_id contract.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { debugError, debugLog } from './logger.mjs';
|
|
15
|
+
import * as api from '../adapters/ticlawk/api.mjs';
|
|
16
|
+
|
|
17
|
+
const SERVER_INFO_CACHE_MS = 30 * 1000;
|
|
18
|
+
const serverInfoCacheByAgent = new Map(); // agentId -> { ts, info }
|
|
19
|
+
|
|
20
|
+
async function getCachedServerInfo(actingAgentId) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const cached = serverInfoCacheByAgent.get(actingAgentId);
|
|
23
|
+
if (cached && now - cached.ts < SERVER_INFO_CACHE_MS) {
|
|
24
|
+
return cached.info;
|
|
25
|
+
}
|
|
26
|
+
const info = await api.getAgentServerInfo({ actingAgentId });
|
|
27
|
+
serverInfoCacheByAgent.set(actingAgentId, { ts: now, info });
|
|
28
|
+
return info;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function invalidateServerInfoCache(actingAgentId = null) {
|
|
32
|
+
if (actingAgentId) serverInfoCacheByAgent.delete(actingAgentId);
|
|
33
|
+
else serverInfoCacheByAgent.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a target string into { conversationId, threadRootMsgId } using a
|
|
38
|
+
* cached server-info lookup. Returns null fields if the target cannot be
|
|
39
|
+
* resolved; callers should treat that as a 404.
|
|
40
|
+
*
|
|
41
|
+
* Target syntax:
|
|
42
|
+
* dm:<uuid> -> conversation_id = <uuid>
|
|
43
|
+
* dm:@<handle> -> find DM conversation whose other member is <handle>
|
|
44
|
+
* #<uuid> -> conversation_id = <uuid>
|
|
45
|
+
* #<group-name> -> find group conversation by name
|
|
46
|
+
* <foo>:<short-msg-id> -> thread under <foo>, root = first message whose
|
|
47
|
+
* id startsWith <short-msg-id>
|
|
48
|
+
*/
|
|
49
|
+
export async function resolveTarget(actingAgentId, target) {
|
|
50
|
+
if (!target) return { conversationId: null, threadRootMsgId: null, error: 'target is required' };
|
|
51
|
+
|
|
52
|
+
// Strip optional thread suffix.
|
|
53
|
+
let base = target;
|
|
54
|
+
let threadShort = null;
|
|
55
|
+
const colonIdx = target.indexOf(':', target.startsWith('dm:') ? 3 : 1);
|
|
56
|
+
if (colonIdx > 0 && colonIdx < target.length - 1) {
|
|
57
|
+
threadShort = target.slice(colonIdx + 1);
|
|
58
|
+
base = target.slice(0, colonIdx);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (/^[0-9a-f-]{36}$/i.test(base)) {
|
|
62
|
+
return { conversationId: base, threadRootMsgId: threadShort, error: null };
|
|
63
|
+
}
|
|
64
|
+
if (base.startsWith('dm:') && /^[0-9a-f-]{36}$/i.test(base.slice(3))) {
|
|
65
|
+
return { conversationId: base.slice(3), threadRootMsgId: threadShort, error: null };
|
|
66
|
+
}
|
|
67
|
+
if (base.startsWith('#') && /^[0-9a-f-]{36}$/i.test(base.slice(1))) {
|
|
68
|
+
return { conversationId: base.slice(1), threadRootMsgId: threadShort, error: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const info = await getCachedServerInfo(actingAgentId);
|
|
72
|
+
const convs = Array.isArray(info?.conversations) ? info.conversations : [];
|
|
73
|
+
|
|
74
|
+
if (base.startsWith('dm:@')) {
|
|
75
|
+
const handle = base.slice(4).toLowerCase();
|
|
76
|
+
const match = convs.find((c) =>
|
|
77
|
+
c.type === 'dm' && (String(c.display_name || c.name || '').toLowerCase() === handle)
|
|
78
|
+
);
|
|
79
|
+
if (match) return { conversationId: match.id, threadRootMsgId: threadShort, error: null };
|
|
80
|
+
return { conversationId: null, threadRootMsgId: null, error: `unknown dm target: ${target}` };
|
|
81
|
+
}
|
|
82
|
+
if (base.startsWith('#')) {
|
|
83
|
+
const name = base.slice(1).toLowerCase();
|
|
84
|
+
const match = convs.find((c) =>
|
|
85
|
+
c.type === 'group' && (String(c.name || c.display_name || '').toLowerCase() === name)
|
|
86
|
+
);
|
|
87
|
+
if (match) return { conversationId: match.id, threadRootMsgId: threadShort, error: null };
|
|
88
|
+
return { conversationId: null, threadRootMsgId: null, error: `unknown group target: ${target}` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { conversationId: null, threadRootMsgId: null, error: `invalid target syntax: ${target}` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getActingAgentId(req, body = {}) {
|
|
95
|
+
const fromHeader = req.headers['x-ticlawk-acting-agent-id'];
|
|
96
|
+
if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
|
|
97
|
+
if (typeof body?.acting_as_agent_id === 'string' && body.acting_as_agent_id.trim()) {
|
|
98
|
+
return body.acting_as_agent_id.trim();
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getRuntimeHostId(req, body = {}) {
|
|
104
|
+
const fromHeader = req.headers['x-ticlawk-runtime-host-id'];
|
|
105
|
+
if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
|
|
106
|
+
if (typeof body?.runtime_host_id === 'string' && body.runtime_host_id.trim()) {
|
|
107
|
+
return body.runtime_host_id.trim();
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateActingAgent(actingAgentId, ctx) {
|
|
113
|
+
if (!actingAgentId) {
|
|
114
|
+
return { ok: false, status: 400, error: 'TICLAWK_RUNTIME_AGENT_ID required (passed via X-Ticlawk-Acting-Agent-Id or body.acting_as_agent_id)' };
|
|
115
|
+
}
|
|
116
|
+
if (typeof ctx.listBindings === 'function') {
|
|
117
|
+
const local = ctx.listBindings({ adapter: 'ticlawk' }) || [];
|
|
118
|
+
const match = local.find((b) => b?.id === actingAgentId);
|
|
119
|
+
if (!match) {
|
|
120
|
+
return { ok: false, status: 401, error: `agent ${actingAgentId} is not bound on this host` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function handleMessageSend(req, body, ctx) {
|
|
127
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
128
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
129
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
130
|
+
|
|
131
|
+
const text = String(body?.text || '').trim();
|
|
132
|
+
if (!text) return { status: 400, body: { error: 'text is required' } };
|
|
133
|
+
|
|
134
|
+
let conversationId = body?.conversation_id || null;
|
|
135
|
+
let threadRootMsgId = null;
|
|
136
|
+
if (!conversationId && body?.target) {
|
|
137
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
138
|
+
if (resolved.error) {
|
|
139
|
+
return { status: 404, body: { error: resolved.error } };
|
|
140
|
+
}
|
|
141
|
+
conversationId = resolved.conversationId;
|
|
142
|
+
threadRootMsgId = resolved.threadRootMsgId;
|
|
143
|
+
}
|
|
144
|
+
if (!conversationId) {
|
|
145
|
+
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const data = await api.sendAgentMessage({
|
|
150
|
+
actingAgentId,
|
|
151
|
+
conversationId,
|
|
152
|
+
text,
|
|
153
|
+
seenUpToSeq: body?.seen_up_to_seq,
|
|
154
|
+
replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
|
|
155
|
+
runtimeHostId: getRuntimeHostId(req, body),
|
|
156
|
+
});
|
|
157
|
+
debugLog('agent-cli', 'send.ok', {
|
|
158
|
+
actingAgentId,
|
|
159
|
+
conversationId,
|
|
160
|
+
messageId: data?.id,
|
|
161
|
+
seq: data?.seq,
|
|
162
|
+
bodyChars: text.length,
|
|
163
|
+
});
|
|
164
|
+
return { status: 200, body: { ok: true, data } };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
debugError('agent-cli', 'send.failed', {
|
|
167
|
+
actingAgentId,
|
|
168
|
+
conversationId,
|
|
169
|
+
error: err?.message || String(err),
|
|
170
|
+
status: err?.status || null,
|
|
171
|
+
});
|
|
172
|
+
return { status: err?.status || 500, body: { error: err?.message || 'send failed', payload: err?.payload || null } };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function handleMessageRead(req, query, ctx) {
|
|
177
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
178
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
179
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
180
|
+
|
|
181
|
+
let conversationId = query?.conversation_id || null;
|
|
182
|
+
if (!conversationId && query?.target) {
|
|
183
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
184
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
185
|
+
conversationId = resolved.conversationId;
|
|
186
|
+
}
|
|
187
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const data = await api.readAgentMessages({
|
|
191
|
+
actingAgentId,
|
|
192
|
+
conversationId,
|
|
193
|
+
aroundMessageId: query?.around_message_id || null,
|
|
194
|
+
beforeSeq: query?.before_seq != null ? Number(query.before_seq) : null,
|
|
195
|
+
limit: query?.limit != null ? Number(query.limit) : null,
|
|
196
|
+
});
|
|
197
|
+
return { status: 200, body: { data } };
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return { status: err?.status || 500, body: { error: err?.message || 'read failed' } };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function handleTaskClaim(req, body, ctx) {
|
|
204
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
205
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
206
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
207
|
+
const sourceMessageId = body?.source_message_id || body?.message_id;
|
|
208
|
+
if (!sourceMessageId) return { status: 400, body: { error: 'source_message_id (or message_id) is required' } };
|
|
209
|
+
try {
|
|
210
|
+
const data = await api.claimAgentTask({
|
|
211
|
+
actingAgentId,
|
|
212
|
+
sourceMessageId,
|
|
213
|
+
leaseSeconds: body?.lease_seconds,
|
|
214
|
+
});
|
|
215
|
+
debugLog('agent-cli', 'task.claim', {
|
|
216
|
+
actingAgentId,
|
|
217
|
+
sourceMessageId,
|
|
218
|
+
claimed: data?.claimed,
|
|
219
|
+
});
|
|
220
|
+
return { status: data?.claimed === false ? 409 : 200, body: data };
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return { status: err?.status || 500, body: { error: err?.message || 'claim failed' } };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function handleTaskUpdate(req, body, ctx) {
|
|
227
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
228
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
229
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
230
|
+
if (!body?.task_id) return { status: 400, body: { error: 'task_id is required' } };
|
|
231
|
+
try {
|
|
232
|
+
const data = await api.updateAgentTask({
|
|
233
|
+
actingAgentId,
|
|
234
|
+
taskId: body.task_id,
|
|
235
|
+
status: body.status || null,
|
|
236
|
+
});
|
|
237
|
+
return { status: 200, body: data };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return { status: err?.status || 500, body: { error: err?.message || 'update failed' } };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function handleTaskList(req, query, ctx) {
|
|
244
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
245
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
246
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
247
|
+
let conversationId = query?.conversation_id || null;
|
|
248
|
+
if (!conversationId && query?.target) {
|
|
249
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
250
|
+
if (!resolved.error) conversationId = resolved.conversationId;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const data = await api.listAgentTasks({ actingAgentId, conversationId });
|
|
254
|
+
return { status: 200, body: { data } };
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return { status: err?.status || 500, body: { error: err?.message || 'list failed' } };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function handleGroupMembers(req, query, ctx) {
|
|
261
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
262
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
263
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
264
|
+
let conversationId = query?.conversation_id || null;
|
|
265
|
+
if (!conversationId && query?.target) {
|
|
266
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
267
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
268
|
+
conversationId = resolved.conversationId;
|
|
269
|
+
}
|
|
270
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
271
|
+
try {
|
|
272
|
+
const res = await api.getConversationMembers({ actingAgentId, conversationId });
|
|
273
|
+
return { status: 200, body: res };
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return { status: err?.status || 500, body: { error: err?.message || 'members lookup failed' } };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function handleServerInfo(req, query, ctx) {
|
|
280
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
281
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
282
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
283
|
+
// Bust cache when CLI explicitly asks
|
|
284
|
+
if (query?.refresh === '1') invalidateServerInfoCache(actingAgentId);
|
|
285
|
+
try {
|
|
286
|
+
const info = await getCachedServerInfo(actingAgentId);
|
|
287
|
+
return { status: 200, body: info };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return { status: err?.status || 500, body: { error: err?.message || 'server-info failed' } };
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/core/config.mjs
CHANGED
|
@@ -17,7 +17,6 @@ export const AF_HOME = process.env.TICLAWK_HOME || join(homedir(), '.ticlawk');
|
|
|
17
17
|
export const AF_CONFIG_PATH = join(AF_HOME, '.config');
|
|
18
18
|
export const AF_LOG_PATH = join(AF_HOME, 'ticlawk.log');
|
|
19
19
|
export const AF_CRASH_LOG_PATH = join(AF_HOME, 'ticlawk-crash.log');
|
|
20
|
-
export const AF_ADAPTER_KEY = 'AF_ADAPTER';
|
|
21
20
|
export const AF_STREAMING_KEY = 'AF_STREAMING';
|
|
22
21
|
export const AF_STREAMING_RUNTIME_KEYS = Object.fromEntries(
|
|
23
22
|
RUNTIME_DEFINITIONS
|
|
@@ -31,28 +30,14 @@ export const RUNTIME_EXECUTABLE_CONFIG_KEYS = Object.fromEntries(
|
|
|
31
30
|
.filter((runtime) => runtime.executableConfigKey)
|
|
32
31
|
.map((runtime) => [runtime.name, runtime.executableConfigKey])
|
|
33
32
|
);
|
|
34
|
-
export const SUPPORTED_ADAPTERS = ['ticlawk', 'telegram'];
|
|
35
33
|
// Public CLI keys stay kebab/dotted for usability; the persisted .config file
|
|
36
34
|
// stores env-style names because systemd/launchd load it directly.
|
|
37
35
|
export const ADAPTER_CONFIG_KEYS = {
|
|
38
|
-
'telegram.bot-token': 'TELEGRAM_BOT_TOKEN',
|
|
39
36
|
'ticlawk.connector-api-key': TICLAWK_CONNECTOR_API_KEY,
|
|
40
37
|
'ticlawk.api-url': 'TICLAWK_API_URL',
|
|
41
38
|
'ticlawk.connector-ws-url': TICLAWK_CONNECTOR_WS_URL,
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
export function normalizeAdapterName(value) {
|
|
45
|
-
const normalized = String(value || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
46
|
-
if (!normalized) return null;
|
|
47
|
-
if (SUPPORTED_ADAPTERS.includes(normalized)) return normalized;
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function getConfiguredAdapter(config = null) {
|
|
52
|
-
const source = config || loadPersistentConfig();
|
|
53
|
-
return normalizeAdapterName(source[AF_ADAPTER_KEY]) || 'ticlawk';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
41
|
export function normalizeAdapterConfigTarget(target) {
|
|
57
42
|
const normalized = String(target || '').trim().toLowerCase();
|
|
58
43
|
const configKey = ADAPTER_CONFIG_KEYS[normalized];
|