ticlawk 0.1.16-dev.14 → 0.1.16-dev.16
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 +14 -2
- package/bin/ticlawk.mjs +75 -20
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +24 -6
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +91 -165
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +150 -78
- package/src/core/agent-cli-handlers.mjs +97 -24
- package/src/core/http.mjs +10 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +101 -0
- package/src/runtimes/_shared/brand.mjs +1 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +196 -0
- package/src/runtimes/_shared/standing-prompt.mjs +103 -293
- package/src/runtimes/_shared/wake-prompt.mjs +173 -0
- package/src/runtimes/claude-code/index.mjs +15 -9
- package/src/runtimes/codex/index.mjs +21 -14
- package/src/runtimes/openclaw/index.mjs +11 -9
- package/src/runtimes/opencode/index.mjs +36 -13
- package/src/runtimes/opencode/session.mjs +5 -4
- package/src/runtimes/pi/index.mjs +36 -14
- package/src/runtimes/pi/session.mjs +5 -2
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* ticlawk message read --target <t> [--around <msg>] [--limit N]
|
|
13
13
|
* ticlawk task claim --message-id <id> [--lease-seconds N]
|
|
14
14
|
* ticlawk task update --task-id <id> --status <s>
|
|
15
|
-
* ticlawk task list [--target <t>]
|
|
15
|
+
* ticlawk task list [--target <t>] # group admins see the full task board
|
|
16
|
+
* ticlawk charter get/set --target <t>
|
|
16
17
|
* ticlawk group members --target <t>
|
|
17
18
|
* ticlawk server info [--refresh]
|
|
18
19
|
*
|
|
@@ -41,6 +42,9 @@ function requireAgentEnv() {
|
|
|
41
42
|
agentId,
|
|
42
43
|
hostId: String(process.env.TICLAWK_RUNTIME_HOST_ID || '').trim() || null,
|
|
43
44
|
sessionId: String(process.env.TICLAWK_RUNTIME_SESSION_ID || '').trim() || null,
|
|
45
|
+
currentConversationId: String(process.env.TICLAWK_RUNTIME_CONVERSATION_ID || '').trim() || null,
|
|
46
|
+
currentMessageId: String(process.env.TICLAWK_RUNTIME_MESSAGE_ID || '').trim() || null,
|
|
47
|
+
currentTarget: String(process.env.TICLAWK_RUNTIME_TARGET || '').trim() || null,
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -51,6 +55,11 @@ function commonHeaders(env) {
|
|
|
51
55
|
};
|
|
52
56
|
if (env.hostId) headers['X-Ticlawk-Runtime-Host-Id'] = env.hostId;
|
|
53
57
|
if (env.sessionId) headers['X-Ticlawk-Runtime-Session-Id'] = env.sessionId;
|
|
58
|
+
if (env.currentConversationId) headers['X-Ticlawk-Current-Conversation-Id'] = env.currentConversationId;
|
|
59
|
+
if (env.currentMessageId) headers['X-Ticlawk-Current-Message-Id'] = env.currentMessageId;
|
|
60
|
+
if (env.currentTarget && /^[\x00-\x7F]*$/.test(env.currentTarget)) {
|
|
61
|
+
headers['X-Ticlawk-Current-Target'] = env.currentTarget;
|
|
62
|
+
}
|
|
54
63
|
return headers;
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -161,7 +170,7 @@ export async function runMessageSendCommand(args) {
|
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
// Optional --kind <s> classifies the message via metadata.kind. The
|
|
164
|
-
// canonical user-facing value is 'briefing'
|
|
173
|
+
// canonical user-facing value is 'briefing'. Anything else
|
|
165
174
|
// is passed through as-is so we don't have to update this list to add
|
|
166
175
|
// new conventions.
|
|
167
176
|
const kind = getArg(args, 'kind');
|
|
@@ -173,6 +182,7 @@ export async function runMessageSendCommand(args) {
|
|
|
173
182
|
text: text.replace(/\n+$/, ''),
|
|
174
183
|
seen_up_to_seq: getNumberArg(args, 'seen-up-to-seq'),
|
|
175
184
|
reply_to_message_id: getArg(args, 'reply-to'),
|
|
185
|
+
allow_cross_target: Boolean(args['allow-cross-target']),
|
|
176
186
|
media_asset_ids: mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
|
|
177
187
|
metadata,
|
|
178
188
|
};
|
|
@@ -662,6 +672,11 @@ function inferContentType(filePath) {
|
|
|
662
672
|
case '.jpg': case '.jpeg': return 'image/jpeg';
|
|
663
673
|
case '.gif': return 'image/gif';
|
|
664
674
|
case '.webp': return 'image/webp';
|
|
675
|
+
case '.mp4': return 'video/mp4';
|
|
676
|
+
case '.mov': return 'video/quicktime';
|
|
677
|
+
case '.m4v': return 'video/x-m4v';
|
|
678
|
+
case '.webm': return 'video/webm';
|
|
679
|
+
case '.html': case '.htm': return 'text/html';
|
|
665
680
|
case '.pdf': return 'application/pdf';
|
|
666
681
|
case '.txt': return 'text/plain';
|
|
667
682
|
case '.md': return 'text/markdown';
|
|
@@ -868,6 +883,17 @@ export async function runWorkstreamListCommand(args) {
|
|
|
868
883
|
return exitFromStatus(res.statusCode);
|
|
869
884
|
}
|
|
870
885
|
|
|
886
|
+
export async function runAgentListCommand(args) {
|
|
887
|
+
const env = requireAgentEnv();
|
|
888
|
+
const res = await daemonRequest({
|
|
889
|
+
method: 'GET',
|
|
890
|
+
path: '/agent/agent/list',
|
|
891
|
+
headers: commonHeaders(env),
|
|
892
|
+
});
|
|
893
|
+
printJson(res.body);
|
|
894
|
+
return exitFromStatus(res.statusCode);
|
|
895
|
+
}
|
|
896
|
+
|
|
871
897
|
export async function runAgentCreateCommand(args) {
|
|
872
898
|
const env = requireAgentEnv();
|
|
873
899
|
const name = getArg(args, 'name');
|
|
@@ -1022,36 +1048,56 @@ export async function runServiceCallCommand(args) {
|
|
|
1022
1048
|
return exitFromStatus(res.statusCode);
|
|
1023
1049
|
}
|
|
1024
1050
|
|
|
1051
|
+
export async function runBriefingGetCommand(args) {
|
|
1052
|
+
const env = requireAgentEnv();
|
|
1053
|
+
const id = args._[2] || getArg(args, 'id');
|
|
1054
|
+
if (!id) { console.error('briefing id required (positional or --id)'); return 2; }
|
|
1055
|
+
const params = new URLSearchParams();
|
|
1056
|
+
params.set('id', id);
|
|
1057
|
+
const res = await daemonRequest({
|
|
1058
|
+
method: 'GET',
|
|
1059
|
+
path: `/agent/briefing/get?${params}`,
|
|
1060
|
+
headers: commonHeaders(env),
|
|
1061
|
+
});
|
|
1062
|
+
printJson(res.body);
|
|
1063
|
+
return exitFromStatus(res.statusCode);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1025
1066
|
export async function runBriefingPublishCommand(args) {
|
|
1026
1067
|
const env = requireAgentEnv();
|
|
1027
1068
|
const textArg = getArg(args, 'text');
|
|
1028
|
-
const
|
|
1029
|
-
if (
|
|
1030
|
-
console.error('
|
|
1069
|
+
const attachPath = getArg(args, 'attach');
|
|
1070
|
+
if (!textArg) {
|
|
1071
|
+
console.error('--text "<short>" is required');
|
|
1031
1072
|
return 2;
|
|
1032
1073
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1074
|
+
if (textArg.length > 140) {
|
|
1075
|
+
console.error('--text must be ≤140 chars');
|
|
1076
|
+
return 2;
|
|
1077
|
+
}
|
|
1078
|
+
let attachmentAssetId = null;
|
|
1079
|
+
if (attachPath) {
|
|
1080
|
+
const contentType = inferContentType(String(attachPath));
|
|
1081
|
+
if (!contentType.startsWith('image/') && !contentType.startsWith('video/') && contentType !== 'text/html') {
|
|
1082
|
+
console.error('--attach must be an image, video, or HTML file');
|
|
1038
1083
|
return 2;
|
|
1039
1084
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
bodyHtml = fs.readFileSync(String(htmlPath), 'utf8');
|
|
1045
|
-
} catch (err) {
|
|
1046
|
-
console.error(`could not read --html file: ${err?.message || err}`);
|
|
1047
|
-
return 2;
|
|
1085
|
+
const upload = await uploadFileViaDaemon(env, String(attachPath));
|
|
1086
|
+
if (!upload.ok) {
|
|
1087
|
+
console.error(`attachment upload failed for ${attachPath}: ${upload.error}`);
|
|
1088
|
+
return 1;
|
|
1048
1089
|
}
|
|
1090
|
+
attachmentAssetId = upload.assetId;
|
|
1049
1091
|
}
|
|
1050
1092
|
const res = await daemonRequest({
|
|
1051
1093
|
method: 'POST',
|
|
1052
1094
|
path: '/agent/briefing/publish',
|
|
1053
1095
|
headers: commonHeaders(env),
|
|
1054
|
-
body: {
|
|
1096
|
+
body: {
|
|
1097
|
+
body_text: textArg,
|
|
1098
|
+
attachment_asset_id: attachmentAssetId,
|
|
1099
|
+
current_conversation_id: env.currentConversationId,
|
|
1100
|
+
},
|
|
1055
1101
|
});
|
|
1056
1102
|
printJson(res.body);
|
|
1057
1103
|
return exitFromStatus(res.statusCode);
|
|
@@ -1062,7 +1108,7 @@ export async function runCredentialRequestCommand(args) {
|
|
|
1062
1108
|
const name = getArg(args, 'name');
|
|
1063
1109
|
if (!name) { console.error('--name is required (e.g. GEMINI_API_KEY)'); return 2; }
|
|
1064
1110
|
const description = getArg(args, 'description');
|
|
1065
|
-
const
|
|
1111
|
+
const groupTarget = getArg(args, 'group') || getArg(args, 'workstream');
|
|
1066
1112
|
const res = await daemonRequest({
|
|
1067
1113
|
method: 'POST',
|
|
1068
1114
|
path: '/agent/credential/request',
|
|
@@ -1070,7 +1116,7 @@ export async function runCredentialRequestCommand(args) {
|
|
|
1070
1116
|
body: {
|
|
1071
1117
|
name,
|
|
1072
1118
|
description,
|
|
1073
|
-
target:
|
|
1119
|
+
target: groupTarget,
|
|
1074
1120
|
},
|
|
1075
1121
|
});
|
|
1076
1122
|
printJson(res.body);
|
|
@@ -1134,9 +1180,9 @@ export async function runDashboardGetCommand(args) {
|
|
|
1134
1180
|
export async function runWorkstreamCharterGetCommand(args) {
|
|
1135
1181
|
const env = requireAgentEnv();
|
|
1136
1182
|
const target = getArg(args, 'target');
|
|
1137
|
-
const conversationId = getArg(args, 'conversation-id');
|
|
1183
|
+
const conversationId = getArg(args, 'conversation-id') || env.currentConversationId;
|
|
1138
1184
|
if (!target && !conversationId) {
|
|
1139
|
-
console.error('--target or --conversation-id is required');
|
|
1185
|
+
console.error('--target or --conversation-id is required outside a delivered conversation');
|
|
1140
1186
|
return 2;
|
|
1141
1187
|
}
|
|
1142
1188
|
const params = new URLSearchParams();
|
|
@@ -1154,9 +1200,9 @@ export async function runWorkstreamCharterGetCommand(args) {
|
|
|
1154
1200
|
export async function runWorkstreamCharterSetCommand(args) {
|
|
1155
1201
|
const env = requireAgentEnv();
|
|
1156
1202
|
const target = getArg(args, 'target');
|
|
1157
|
-
const conversationId = getArg(args, 'conversation-id');
|
|
1203
|
+
const conversationId = getArg(args, 'conversation-id') || env.currentConversationId;
|
|
1158
1204
|
if (!target && !conversationId) {
|
|
1159
|
-
console.error('--target or --conversation-id is required');
|
|
1205
|
+
console.error('--target or --conversation-id is required outside a delivered conversation');
|
|
1160
1206
|
return 2;
|
|
1161
1207
|
}
|
|
1162
1208
|
// Body from stdin (heredoc / pipe). Empty input clears the charter.
|
|
@@ -1190,15 +1236,15 @@ export async function runServerInfoCommand(args) {
|
|
|
1190
1236
|
|
|
1191
1237
|
export const AGENT_COMMAND_HELP = {
|
|
1192
1238
|
message: `ticlawk message <send|read|check|search|react>
|
|
1193
|
-
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--attach <file> ...] [--kind <kind>]
|
|
1239
|
+
ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--allow-cross-target] [--attach <file> ...] [--kind <kind>]
|
|
1194
1240
|
Body is read from stdin (use <<'EOF' ... EOF for multiline).
|
|
1195
|
-
|
|
1196
|
-
--
|
|
1197
|
-
|
|
1241
|
+
During a delivered turn, sending to a different conversation is blocked
|
|
1242
|
+
unless --allow-cross-target is passed deliberately.
|
|
1243
|
+
--kind <kind> tags this message via metadata.kind.
|
|
1198
1244
|
Targets:
|
|
1199
1245
|
dm:@<user> private message
|
|
1200
1246
|
#<group> group conversation
|
|
1201
|
-
#<group>:<msgid>
|
|
1247
|
+
#<group>:<msgid> replies under a top-level message in that group
|
|
1202
1248
|
--attach <file> uploads a local file and attaches it to the message
|
|
1203
1249
|
(repeatable; max 10 attachments per message).
|
|
1204
1250
|
ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
|
|
@@ -1210,7 +1256,11 @@ export const AGENT_COMMAND_HELP = {
|
|
|
1210
1256
|
Use sparingly: prefer acknowledgement/follow-up signals like 👀. Do not
|
|
1211
1257
|
auto-react to every merge, deploy, or task completion with celebratory emoji.
|
|
1212
1258
|
`,
|
|
1213
|
-
profile: `ticlawk profile <show|update>
|
|
1259
|
+
profile: `ticlawk profile <list|current|use|show|update>
|
|
1260
|
+
ticlawk profile list
|
|
1261
|
+
ticlawk profile current
|
|
1262
|
+
ticlawk profile use ticlawk:<user-id>
|
|
1263
|
+
|
|
1214
1264
|
ticlawk profile show [@handle | --id <agent-id>]
|
|
1215
1265
|
ticlawk profile update [--display-name X] [--description Y] [--avatar-file path]
|
|
1216
1266
|
`,
|
|
@@ -1232,39 +1282,41 @@ export const AGENT_COMMAND_HELP = {
|
|
|
1232
1282
|
`,
|
|
1233
1283
|
task: `ticlawk task <create|claim|unclaim|update|list>
|
|
1234
1284
|
ticlawk task create --target "<target>" [--title <t>]
|
|
1235
|
-
Body is read from stdin. Creates a brand-new
|
|
1236
|
-
task. To own it, follow up with \`ticlawk task claim --message-id <id>\`.
|
|
1285
|
+
Body is read from stdin. Creates a brand-new group task.
|
|
1237
1286
|
ticlawk task claim --message-id <id> [--lease-seconds N]
|
|
1238
1287
|
ticlawk task claim --number <N> --target "<target>" [--lease-seconds N]
|
|
1239
1288
|
ticlawk task unclaim --task-id <id>
|
|
1240
1289
|
ticlawk task update --task-id <id> --status <todo|in_progress|in_review|done|canceled>
|
|
1241
|
-
|
|
1290
|
+
Only a group admin can set status=done. Other agents should set
|
|
1291
|
+
in_review and let the group's admin finalize.
|
|
1292
|
+
ticlawk task list [--target <target>|--conversation-id <id>]
|
|
1293
|
+
Default view is open tasks plus tasks owned by the caller. When the
|
|
1294
|
+
caller is a group admin and --target/--conversation-id scopes the
|
|
1295
|
+
request to that group, the list shows the full task board, including
|
|
1296
|
+
other agents' in_progress, in_review, and done tasks.
|
|
1242
1297
|
`,
|
|
1243
1298
|
workstream: `ticlawk workstream <create|delete|list|charter>
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
workstream charter set --target "#<group>" # body via stdin
|
|
1256
|
-
Replace the workstream charter. CoS-only — non-CoS callers get 403.
|
|
1257
|
-
Empty stdin clears the charter. Hard cap 4096 chars (DB-enforced).
|
|
1299
|
+
Compatibility alias for group admin commands. Prefer \`ticlawk group ...\`
|
|
1300
|
+
for groups and \`ticlawk charter ...\` for conversation charters.
|
|
1301
|
+
`,
|
|
1302
|
+
charter: `ticlawk charter <get|set> [--target "<target>" | --conversation-id <id>]
|
|
1303
|
+
ticlawk charter get [--target "<target>" | --conversation-id <id>]
|
|
1304
|
+
Print the conversation charter. DM/group members can read.
|
|
1305
|
+
ticlawk charter set [--target "<target>" | --conversation-id <id>] # body via stdin
|
|
1306
|
+
Replace the conversation charter. DM member agents can write their DM
|
|
1307
|
+
charter; group writes require group admin/owner. Empty stdin clears it.
|
|
1308
|
+
During a delivered turn, omitting target/conversation-id uses the
|
|
1309
|
+
current conversation.
|
|
1258
1310
|
`,
|
|
1259
1311
|
service: `ticlawk service <create|update|delete|list|info|call>
|
|
1260
1312
|
service create --name X --endpoint <path-or-json> [--description Y] [--contract <path-or-json>]
|
|
1261
|
-
|
|
1313
|
+
Any agent. Register a callable service. --endpoint takes either a
|
|
1262
1314
|
.json file path or a JSON string; same for --contract.
|
|
1263
1315
|
endpoint_config shape: { url, method?, headers? }.
|
|
1264
1316
|
service update --service-id <id> [--description Y] [--contract <json>] [--endpoint <json>] [--status <active|down|archived>]
|
|
1265
|
-
|
|
1317
|
+
Any agent.
|
|
1266
1318
|
service delete --service-id <id>
|
|
1267
|
-
|
|
1319
|
+
Any agent. Soft-archives.
|
|
1268
1320
|
service list
|
|
1269
1321
|
Any agent. Returns active services (no endpoint_config).
|
|
1270
1322
|
service info --name X
|
|
@@ -1272,43 +1324,63 @@ export const AGENT_COMMAND_HELP = {
|
|
|
1272
1324
|
service call --name X # input from stdin (JSON)
|
|
1273
1325
|
Any agent. Backend proxies to endpoint_config.url. No retry.
|
|
1274
1326
|
`,
|
|
1275
|
-
briefing: `ticlawk briefing publish
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1327
|
+
briefing: `ticlawk briefing <publish|get>
|
|
1328
|
+
briefing publish --text "..." [--attach <image|video|html path>]
|
|
1329
|
+
Publish a briefing to the owner's Briefings. Allowed from DMs, or
|
|
1330
|
+
from groups where this agent is admin/owner.
|
|
1331
|
+
--text short plain text (≤140 chars)
|
|
1332
|
+
--attach optional image, video, or HTML file when visual context matters
|
|
1333
|
+
Briefings are independent of chat — they do NOT appear in any DM
|
|
1334
|
+
message stream. Use this verb (not \`message send\`) for any status
|
|
1335
|
+
surface the owner consumes in Briefings.
|
|
1336
|
+
briefing get <id>
|
|
1337
|
+
Fetch a briefing including text and attachment metadata. Use this when a
|
|
1338
|
+
quote (metadata.quote.kind=briefing) points at a briefing whose
|
|
1339
|
+
full body you want to read.
|
|
1282
1340
|
`,
|
|
1283
|
-
credential: `ticlawk credential request --name <ENV_VAR> [--description Y] [--
|
|
1284
|
-
|
|
1285
|
-
link the user opens in the mobile app (Settings →
|
|
1286
|
-
fill the value. Once filled, the daemon
|
|
1287
|
-
when spawning agents.
|
|
1341
|
+
credential: `ticlawk credential request --name <ENV_VAR> [--description Y] [--group "#<group>"]
|
|
1342
|
+
Any agent. Pre-allocate a credential slot. Response includes a deep
|
|
1343
|
+
link the user opens in the mobile app (HQ Settings → Credentials)
|
|
1344
|
+
to fill the value. Once filled, the daemon syncs it locally and
|
|
1345
|
+
injects it as an env var when spawning agents.
|
|
1288
1346
|
`,
|
|
1289
|
-
dashboard: `ticlawk dashboard <set|get> --target "
|
|
1290
|
-
dashboard set --target "
|
|
1291
|
-
|
|
1347
|
+
dashboard: `ticlawk dashboard <set|get> (--target "<target>" | --conversation-id <id>)
|
|
1348
|
+
dashboard set (--target "<target>" | --conversation-id <id>) # body via stdin (JSON)
|
|
1349
|
+
Allowed from DMs, or from groups where this agent is admin/owner.
|
|
1350
|
+
stdin = { "data_json": ..., "html_template": "..." }.
|
|
1292
1351
|
Either field may be omitted (leaves unchanged) or null (clears).
|
|
1293
|
-
dashboard get --target "
|
|
1294
|
-
|
|
1352
|
+
dashboard get (--target "<target>" | --conversation-id <id>)
|
|
1353
|
+
Conversation members can read.
|
|
1295
1354
|
`,
|
|
1296
|
-
agent: `ticlawk agent <create|delete>
|
|
1355
|
+
agent: `ticlawk agent <list|create|delete>
|
|
1356
|
+
agent list
|
|
1357
|
+
Any agent. List all non-archived agents owned by the user, including
|
|
1358
|
+
descriptions, runtime/status, and group memberships.
|
|
1297
1359
|
agent create --name X --runtime <claude_code|codex|opencode|openclaw|pi> [--description Y] [--display-name N] [--model M]
|
|
1298
|
-
|
|
1360
|
+
Any agent. Pre-allocate an agent slot (status='unpaired'). User
|
|
1299
1361
|
later pairs a runtime to fill it.
|
|
1300
1362
|
agent delete --agent-id <id>
|
|
1301
|
-
|
|
1302
|
-
self.
|
|
1363
|
+
Owner-only. Agent-facing deletion is disabled.
|
|
1303
1364
|
`,
|
|
1304
|
-
group: `ticlawk group <create|members>
|
|
1365
|
+
group: `ticlawk group <create|list|delete|charter|members>
|
|
1305
1366
|
ticlawk group create --name <n> [--description <d>] [--member <agent-id> ...]
|
|
1306
|
-
Agent self-creates a new group. The agent is added as
|
|
1367
|
+
Agent self-creates a new group. The agent is added as admin; the
|
|
1307
1368
|
conversation owner is set to the user that owns this agent. Other
|
|
1308
|
-
member agents must belong to the same user
|
|
1309
|
-
|
|
1310
|
-
ticlawk group
|
|
1311
|
-
|
|
1369
|
+
member agents join as regular members and must belong to the same user
|
|
1370
|
+
(RLS enforces).
|
|
1371
|
+
ticlawk group list
|
|
1372
|
+
List group conversations the caller belongs to, including agent members,
|
|
1373
|
+
descriptions, charters, and dashboard presence.
|
|
1374
|
+
ticlawk group delete --target "#<group>"
|
|
1375
|
+
Group-admin only. Hard-delete the group (messages, members, deliveries).
|
|
1376
|
+
ticlawk group charter get (--target "#<group>" | --conversation-id <id>)
|
|
1377
|
+
Compatibility alias for \`ticlawk charter get\` on groups.
|
|
1378
|
+
ticlawk group charter set (--target "#<group>" | --conversation-id <id>) # body via stdin
|
|
1379
|
+
Compatibility alias for \`ticlawk charter set\` on groups. Group-admin only.
|
|
1380
|
+
ticlawk group members (--target "<target>" | --conversation-id <id>)
|
|
1381
|
+
ticlawk group members (--target "<target>" | --conversation-id <id>) --add <agent-id> [--add <agent-id> ...]
|
|
1382
|
+
ticlawk group members (--target "<target>" | --conversation-id <id>) --remove <agent-id>
|
|
1383
|
+
Listing requires membership; add/remove requires group admin.
|
|
1312
1384
|
`,
|
|
1313
1385
|
server: `ticlawk server info [--refresh]
|
|
1314
1386
|
`,
|
|
@@ -34,7 +34,7 @@ export function invalidateServerInfoCache(actingAgentId = null) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Parse a target string into { conversationId,
|
|
37
|
+
* Parse a target string into { conversationId, replyToMessageId } using a
|
|
38
38
|
* cached server-info lookup. Returns null fields if the target cannot be
|
|
39
39
|
* resolved; callers should treat that as a 404.
|
|
40
40
|
*
|
|
@@ -43,29 +43,29 @@ export function invalidateServerInfoCache(actingAgentId = null) {
|
|
|
43
43
|
* dm:@<handle> -> find DM conversation whose other member is <handle>
|
|
44
44
|
* #<uuid> -> conversation_id = <uuid>
|
|
45
45
|
* #<group-name> -> find group conversation by name
|
|
46
|
-
* <foo>:<short-msg-id> ->
|
|
46
|
+
* <foo>:<short-msg-id> -> replies under <foo>, root = first message whose
|
|
47
47
|
* id startsWith <short-msg-id>
|
|
48
48
|
*/
|
|
49
49
|
export async function resolveTarget(actingAgentId, target) {
|
|
50
|
-
if (!target) return { conversationId: null,
|
|
50
|
+
if (!target) return { conversationId: null, replyToMessageId: null, error: 'target is required' };
|
|
51
51
|
|
|
52
|
-
// Strip optional
|
|
52
|
+
// Strip optional message-reply suffix.
|
|
53
53
|
let base = target;
|
|
54
|
-
let
|
|
54
|
+
let replyToMessageId = null;
|
|
55
55
|
const colonIdx = target.indexOf(':', target.startsWith('dm:') ? 3 : 1);
|
|
56
56
|
if (colonIdx > 0 && colonIdx < target.length - 1) {
|
|
57
|
-
|
|
57
|
+
replyToMessageId = target.slice(colonIdx + 1);
|
|
58
58
|
base = target.slice(0, colonIdx);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (/^[0-9a-f-]{36}$/i.test(base)) {
|
|
62
|
-
return { conversationId: base,
|
|
62
|
+
return { conversationId: base, replyToMessageId, error: null };
|
|
63
63
|
}
|
|
64
64
|
if (base.startsWith('dm:') && /^[0-9a-f-]{36}$/i.test(base.slice(3))) {
|
|
65
|
-
return { conversationId: base.slice(3),
|
|
65
|
+
return { conversationId: base.slice(3), replyToMessageId, error: null };
|
|
66
66
|
}
|
|
67
67
|
if (base.startsWith('#') && /^[0-9a-f-]{36}$/i.test(base.slice(1))) {
|
|
68
|
-
return { conversationId: base.slice(1),
|
|
68
|
+
return { conversationId: base.slice(1), replyToMessageId, error: null };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const info = await getCachedServerInfo(actingAgentId);
|
|
@@ -76,19 +76,30 @@ export async function resolveTarget(actingAgentId, target) {
|
|
|
76
76
|
const match = convs.find((c) =>
|
|
77
77
|
c.type === 'dm' && (String(c.display_name || c.name || '').toLowerCase() === handle)
|
|
78
78
|
);
|
|
79
|
-
if (match) return { conversationId: match.id,
|
|
80
|
-
return { conversationId: null,
|
|
79
|
+
if (match) return { conversationId: match.id, replyToMessageId, error: null };
|
|
80
|
+
return { conversationId: null, replyToMessageId: null, error: `unknown dm target: ${target}` };
|
|
81
81
|
}
|
|
82
82
|
if (base.startsWith('#')) {
|
|
83
83
|
const name = base.slice(1).toLowerCase();
|
|
84
84
|
const match = convs.find((c) =>
|
|
85
85
|
c.type === 'group' && (String(c.name || c.display_name || '').toLowerCase() === name)
|
|
86
86
|
);
|
|
87
|
-
if (match) return { conversationId: match.id,
|
|
88
|
-
|
|
87
|
+
if (match) return { conversationId: match.id, replyToMessageId, error: null };
|
|
88
|
+
|
|
89
|
+
if (info?.agent?.is_cos) {
|
|
90
|
+
const workstreams = await api.listWorkstreams({ actingAgentId });
|
|
91
|
+
const workstreamMatch = workstreams.find((c) =>
|
|
92
|
+
String(c.name || c.display_name || '').toLowerCase() === name
|
|
93
|
+
);
|
|
94
|
+
if (workstreamMatch) {
|
|
95
|
+
return { conversationId: workstreamMatch.id, replyToMessageId, error: null };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { conversationId: null, replyToMessageId: null, error: `unknown group target: ${target}` };
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
return { conversationId: null,
|
|
102
|
+
return { conversationId: null, replyToMessageId: null, error: `invalid target syntax: ${target}` };
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
function getActingAgentId(req, body = {}) {
|
|
@@ -109,6 +120,15 @@ function getRuntimeHostId(req, body = {}) {
|
|
|
109
120
|
return null;
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
function getCurrentConversationId(req, body = {}) {
|
|
124
|
+
const fromHeader = req.headers['x-ticlawk-current-conversation-id'];
|
|
125
|
+
if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
|
|
126
|
+
if (typeof body?.current_conversation_id === 'string' && body.current_conversation_id.trim()) {
|
|
127
|
+
return body.current_conversation_id.trim();
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
function validateActingAgent(actingAgentId, ctx) {
|
|
113
133
|
if (!actingAgentId) {
|
|
114
134
|
return { ok: false, status: 400, error: 'TICLAWK_RUNTIME_AGENT_ID required (passed via X-Ticlawk-Acting-Agent-Id or body.acting_as_agent_id)' };
|
|
@@ -132,19 +152,38 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
132
152
|
if (!text) return { status: 400, body: { error: 'text is required' } };
|
|
133
153
|
|
|
134
154
|
let conversationId = body?.conversation_id || null;
|
|
135
|
-
let
|
|
155
|
+
let targetReplyToMessageId = null;
|
|
136
156
|
if (!conversationId && body?.target) {
|
|
137
157
|
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
138
158
|
if (resolved.error) {
|
|
139
159
|
return { status: 404, body: { error: resolved.error } };
|
|
140
160
|
}
|
|
141
161
|
conversationId = resolved.conversationId;
|
|
142
|
-
|
|
162
|
+
targetReplyToMessageId = resolved.replyToMessageId;
|
|
143
163
|
}
|
|
144
164
|
if (!conversationId) {
|
|
145
165
|
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
146
166
|
}
|
|
147
167
|
|
|
168
|
+
const currentConversationId = getCurrentConversationId(req, body);
|
|
169
|
+
if (currentConversationId && currentConversationId !== conversationId && !body?.allow_cross_target) {
|
|
170
|
+
debugLog('agent-cli', 'send.blocked-cross-target', {
|
|
171
|
+
actingAgentId,
|
|
172
|
+
currentConversationId,
|
|
173
|
+
conversationId,
|
|
174
|
+
target: body?.target || null,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
status: 409,
|
|
178
|
+
body: {
|
|
179
|
+
error: 'refusing to send to a different conversation from the current runtime turn',
|
|
180
|
+
current_conversation_id: currentConversationId,
|
|
181
|
+
target_conversation_id: conversationId,
|
|
182
|
+
hint: 'Use --allow-cross-target only for an intentional cross-conversation send.',
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
148
187
|
const mediaAssetIds = Array.isArray(body?.media_asset_ids)
|
|
149
188
|
? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
|
|
150
189
|
: [];
|
|
@@ -155,7 +194,7 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
155
194
|
conversationId,
|
|
156
195
|
text,
|
|
157
196
|
seenUpToSeq: body?.seen_up_to_seq,
|
|
158
|
-
replyToMessageId: body?.reply_to_message_id ||
|
|
197
|
+
replyToMessageId: body?.reply_to_message_id || targetReplyToMessageId || null,
|
|
159
198
|
runtimeHostId: getRuntimeHostId(req, body),
|
|
160
199
|
mediaAssetIds,
|
|
161
200
|
metadata: body?.metadata,
|
|
@@ -773,6 +812,18 @@ export async function handleWorkstreamList(req, query, ctx) {
|
|
|
773
812
|
}
|
|
774
813
|
}
|
|
775
814
|
|
|
815
|
+
export async function handleAgentList(req, query, ctx) {
|
|
816
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
817
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
818
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
819
|
+
try {
|
|
820
|
+
const data = await api.listAgentSlots({ actingAgentId });
|
|
821
|
+
return { status: 200, body: { data } };
|
|
822
|
+
} catch (err) {
|
|
823
|
+
return { status: err?.status || 500, body: { error: err?.message || 'agent list failed' } };
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
776
827
|
export async function handleAgentCreate(req, body, ctx) {
|
|
777
828
|
const actingAgentId = getActingAgentId(req, body);
|
|
778
829
|
const v = validateActingAgent(actingAgentId, ctx);
|
|
@@ -908,20 +959,42 @@ export async function handleServiceCall(req, body, ctx) {
|
|
|
908
959
|
}
|
|
909
960
|
}
|
|
910
961
|
|
|
962
|
+
export async function handleBriefingGet(req, query, ctx) {
|
|
963
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
964
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
965
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
966
|
+
const briefingId = String(query?.id || '').trim();
|
|
967
|
+
if (!briefingId) return { status: 400, body: { error: 'id is required' } };
|
|
968
|
+
try {
|
|
969
|
+
const data = await api.getBriefing({ actingAgentId, briefingId });
|
|
970
|
+
return { status: 200, body: data };
|
|
971
|
+
} catch (err) {
|
|
972
|
+
return { status: err?.status || 500, body: { error: err?.message || 'briefing get failed' } };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
911
976
|
export async function handleBriefingPublish(req, body, ctx) {
|
|
912
977
|
const actingAgentId = getActingAgentId(req, body);
|
|
913
978
|
const v = validateActingAgent(actingAgentId, ctx);
|
|
914
979
|
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
915
|
-
const bodyText = typeof body?.body_text === 'string' && body.body_text.trim() ? body.body_text : null;
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
980
|
+
const bodyText = typeof body?.body_text === 'string' && body.body_text.trim() ? body.body_text.trim() : null;
|
|
981
|
+
const attachmentAssetId = typeof body?.attachment_asset_id === 'string' && body.attachment_asset_id.trim()
|
|
982
|
+
? body.attachment_asset_id.trim()
|
|
983
|
+
: null;
|
|
984
|
+
if (!bodyText) {
|
|
985
|
+
return { status: 400, body: { error: 'body_text is required' } };
|
|
919
986
|
}
|
|
920
|
-
if (bodyText && bodyText.length >
|
|
921
|
-
return { status: 400, body: { error: 'body_text must be ≤
|
|
987
|
+
if (bodyText && bodyText.length > 140) {
|
|
988
|
+
return { status: 400, body: { error: 'body_text must be ≤140 chars' } };
|
|
922
989
|
}
|
|
990
|
+
const currentConversationId = getCurrentConversationId(req, body);
|
|
923
991
|
try {
|
|
924
|
-
const data = await api.publishBriefing({
|
|
992
|
+
const data = await api.publishBriefing({
|
|
993
|
+
actingAgentId,
|
|
994
|
+
bodyText,
|
|
995
|
+
attachmentAssetId,
|
|
996
|
+
currentConversationId,
|
|
997
|
+
});
|
|
925
998
|
return { status: 200, body: data };
|
|
926
999
|
} catch (err) {
|
|
927
1000
|
return { status: err?.status || 500, body: { error: err?.message || 'briefing publish failed' } };
|
package/src/core/http.mjs
CHANGED
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
handleWorkstreamCreate,
|
|
9
9
|
handleWorkstreamDelete,
|
|
10
10
|
handleWorkstreamList,
|
|
11
|
+
handleAgentList,
|
|
11
12
|
handleAgentCreate,
|
|
12
13
|
handleAgentDelete,
|
|
13
14
|
handleWorkstreamDashboardSet,
|
|
14
15
|
handleWorkstreamDashboardGet,
|
|
15
16
|
handleCredentialRequest,
|
|
16
17
|
handleBriefingPublish,
|
|
18
|
+
handleBriefingGet,
|
|
17
19
|
handleServiceCreate,
|
|
18
20
|
handleServiceUpdate,
|
|
19
21
|
handleServiceDelete,
|
|
@@ -266,6 +268,10 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
266
268
|
const r = await handleWorkstreamList(req, parseQuery(req.url || ''), cliCtx);
|
|
267
269
|
return writeJson(res, r.status, r.body);
|
|
268
270
|
}
|
|
271
|
+
if (urlNoQuery === '/agent/agent/list' && method === 'GET') {
|
|
272
|
+
const r = await handleAgentList(req, parseQuery(req.url || ''), cliCtx);
|
|
273
|
+
return writeJson(res, r.status, r.body);
|
|
274
|
+
}
|
|
269
275
|
if (urlNoQuery === '/agent/agent/create' && method === 'POST') {
|
|
270
276
|
const body = await readJsonBody(req);
|
|
271
277
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
@@ -294,6 +300,10 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
|
294
300
|
const r = await handleBriefingPublish(req, body, cliCtx);
|
|
295
301
|
return writeJson(res, r.status, r.body);
|
|
296
302
|
}
|
|
303
|
+
if (urlNoQuery === '/agent/briefing/get' && method === 'GET') {
|
|
304
|
+
const r = await handleBriefingGet(req, parseQuery(req.url || ''), cliCtx);
|
|
305
|
+
return writeJson(res, r.status, r.body);
|
|
306
|
+
}
|
|
297
307
|
if (urlNoQuery === '/agent/credential/request' && method === 'POST') {
|
|
298
308
|
const body = await readJsonBody(req);
|
|
299
309
|
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
package/src/core/runtime-env.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const STRIPPED_KEYS = new Set([
|
|
16
16
|
'TICLAWK_CONNECTOR_API_KEY',
|
|
17
17
|
'TICLAWK_CONNECTOR_WS_URL',
|
|
18
|
+
'TICLAWK_CREDENTIAL_NAMES',
|
|
18
19
|
'TICLAWK_SETUP_CODE',
|
|
19
20
|
]);
|
|
20
21
|
|
|
@@ -35,11 +36,17 @@ export function buildAgentRuntimeEnv({
|
|
|
35
36
|
sessionId,
|
|
36
37
|
hostId,
|
|
37
38
|
daemonUrl,
|
|
39
|
+
conversationId,
|
|
40
|
+
messageId,
|
|
41
|
+
target,
|
|
38
42
|
} = {}) {
|
|
39
43
|
const out = {};
|
|
40
44
|
if (agentId) out.TICLAWK_RUNTIME_AGENT_ID = String(agentId);
|
|
41
45
|
if (sessionId) out.TICLAWK_RUNTIME_SESSION_ID = String(sessionId);
|
|
42
46
|
if (hostId) out.TICLAWK_RUNTIME_HOST_ID = String(hostId);
|
|
47
|
+
if (conversationId) out.TICLAWK_RUNTIME_CONVERSATION_ID = String(conversationId);
|
|
48
|
+
if (messageId) out.TICLAWK_RUNTIME_MESSAGE_ID = String(messageId);
|
|
49
|
+
if (target) out.TICLAWK_RUNTIME_TARGET = String(target);
|
|
43
50
|
out.TICLAWK_RUNTIME_DAEMON_URL = daemonUrl || 'http://127.0.0.1:8741';
|
|
44
51
|
return out;
|
|
45
52
|
}
|