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.
@@ -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' (Office Briefings surface). Anything else
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 htmlPath = getArg(args, 'html');
1029
- if ((textArg && htmlPath) || (!textArg && !htmlPath)) {
1030
- console.error('exactly one of --text "<short>" or --html <path> is required');
1069
+ const attachPath = getArg(args, 'attach');
1070
+ if (!textArg) {
1071
+ console.error('--text "<short>" is required');
1031
1072
  return 2;
1032
1073
  }
1033
- let bodyText = null;
1034
- let bodyHtml = null;
1035
- if (textArg) {
1036
- if (textArg.length > 100) {
1037
- console.error('--text must be ≤100 chars');
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
- bodyText = textArg;
1041
- } else {
1042
- try {
1043
- const fs = await import('node:fs');
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: { body_text: bodyText, body_html: bodyHtml },
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 workstream = getArg(args, 'workstream');
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: workstream,
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
- --kind <kind> tags this message via metadata.kind. The CoS uses
1196
- --kind briefing to publish a status briefing that the Office
1197
- tab can list separately.
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> thread under a top-level message in that group
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 message published as a todo
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
- ticlawk task list [--target <target>]
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
- workstream create --name X [--description Y] [--member <agent-id> ...] [--charter [<text>]]
1245
- CoS-only. Create a managed group conversation. If --charter has no
1246
- value, body is read from stdin (heredoc / pipe). Empty stdin = no
1247
- charter. Members are joined alongside CoS.
1248
- workstream delete --target "#<group>"
1249
- CoS-only. Hard-delete the workstream (messages, members, deliveries
1250
- cascade). Use carefully.
1251
- workstream list
1252
- List all workstreams (group conversations) owned by your user.
1253
- workstream charter get --target "#<group>"
1254
- Print the workstream's charter (CoS-authored markdown).
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
- CoS-only. Register a callable service. --endpoint takes either a
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
- CoS-only.
1317
+ Any agent.
1266
1318
  service delete --service-id <id>
1267
- CoS-only. Soft-archives.
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 (--text "..." | --html <path>)
1276
- CoS-only. Publish a briefing to the owner's Office Briefings.
1277
- --text short plain text (≤100 chars), use for one-liner pings
1278
- --html path to an HTML file; body is rendered as a full-screen card
1279
- Briefings are independent of chat — they do NOT appear in the CoS
1280
- DM message stream. Use this verb (not \`message send\`) for any
1281
- status surface the owner consumes in Office.
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] [--workstream "#<ws>"]
1284
- CoS-only. Pre-allocate a credential slot. Response includes a deep
1285
- link the user opens in the mobile app (Settings → Connections) to
1286
- fill the value. Once filled, the daemon injects it as an env var
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 "#<workstream>"
1290
- dashboard set --target "#<workstream>" # body via stdin (JSON)
1291
- CoS-only. stdin = { "data_json": ..., "html_template": "..." }.
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 "#<workstream>"
1294
- Any member of the workstream can read.
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
- CoS-only. Pre-allocate an agent slot (status='unpaired'). User
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
- CoS-only. Soft-delete the agent (status='archived'). Cannot archive
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 a member; the
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 (RLS enforces).
1309
- ticlawk group members --target "<target>"
1310
- ticlawk group members --target "<target>" --add <agent-id> [--add <agent-id> ...]
1311
- ticlawk group members --target "<target>" --remove <agent-id>
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, threadRootMsgId } using a
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> -> thread under <foo>, root = first message whose
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, threadRootMsgId: null, error: 'target is required' };
50
+ if (!target) return { conversationId: null, replyToMessageId: null, error: 'target is required' };
51
51
 
52
- // Strip optional thread suffix.
52
+ // Strip optional message-reply suffix.
53
53
  let base = target;
54
- let threadShort = null;
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
- threadShort = target.slice(colonIdx + 1);
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, threadRootMsgId: threadShort, error: null };
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), threadRootMsgId: threadShort, error: null };
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), threadRootMsgId: threadShort, error: null };
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, threadRootMsgId: threadShort, error: null };
80
- return { conversationId: null, threadRootMsgId: null, error: `unknown dm target: ${target}` };
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, threadRootMsgId: threadShort, error: null };
88
- return { conversationId: null, threadRootMsgId: null, error: `unknown group target: ${target}` };
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, threadRootMsgId: null, error: `invalid target syntax: ${target}` };
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 threadRootMsgId = null;
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
- threadRootMsgId = resolved.threadRootMsgId;
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 || threadRootMsgId || null,
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 bodyHtml = typeof body?.body_html === 'string' && body.body_html.trim() ? body.body_html : null;
917
- if ((bodyText && bodyHtml) || (!bodyText && !bodyHtml)) {
918
- return { status: 400, body: { error: 'exactly one of body_text or body_html is required' } };
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 > 100) {
921
- return { status: 400, body: { error: 'body_text must be ≤100 chars' } };
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({ actingAgentId, bodyText, bodyHtml });
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' });
@@ -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
  }