ticlawk 0.1.16-dev.11 → 0.1.16-dev.12

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/bin/ticlawk.mjs CHANGED
@@ -32,6 +32,22 @@ import {
32
32
  AGENT_COMMAND_HELP,
33
33
  runAttachmentViewCommand,
34
34
  runGroupCreateCommand,
35
+ runWorkstreamCharterGetCommand,
36
+ runWorkstreamCharterSetCommand,
37
+ runWorkstreamCreateCommand,
38
+ runWorkstreamDeleteCommand,
39
+ runWorkstreamListCommand,
40
+ runAgentCreateCommand,
41
+ runAgentDeleteCommand,
42
+ runDashboardSetCommand,
43
+ runDashboardGetCommand,
44
+ runCredentialRequestCommand,
45
+ runServiceCreateCommand,
46
+ runServiceUpdateCommand,
47
+ runServiceDeleteCommand,
48
+ runServiceListCommand,
49
+ runServiceInfoCommand,
50
+ runServiceCallCommand,
35
51
  runGroupMembersAddCommand,
36
52
  runGroupMembersCommand,
37
53
  runGroupMembersRemoveCommand,
@@ -453,6 +469,107 @@ async function main() {
453
469
  process.exit(1);
454
470
  }
455
471
 
472
+ if (command === 'workstream') {
473
+ const sub = args._[1];
474
+ if (args.help || args.h || !sub) {
475
+ console.log(AGENT_COMMAND_HELP.workstream);
476
+ return;
477
+ }
478
+ if (sub === 'create') {
479
+ process.exitCode = await runWorkstreamCreateCommand(args);
480
+ return;
481
+ }
482
+ if (sub === 'delete') {
483
+ process.exitCode = await runWorkstreamDeleteCommand(args);
484
+ return;
485
+ }
486
+ if (sub === 'list') {
487
+ process.exitCode = await runWorkstreamListCommand(args);
488
+ return;
489
+ }
490
+ if (sub === 'charter') {
491
+ const op = args._[2];
492
+ if (op === 'get') {
493
+ process.exitCode = await runWorkstreamCharterGetCommand(args);
494
+ return;
495
+ }
496
+ if (op === 'set') {
497
+ process.exitCode = await runWorkstreamCharterSetCommand(args);
498
+ return;
499
+ }
500
+ console.error(`unknown workstream charter op: ${op}`);
501
+ process.exit(1);
502
+ }
503
+ console.error(`unknown workstream subcommand: ${sub}`);
504
+ process.exit(1);
505
+ }
506
+
507
+ if (command === 'agent') {
508
+ const sub = args._[1];
509
+ if (args.help || args.h || !sub) {
510
+ console.log(AGENT_COMMAND_HELP.agent);
511
+ return;
512
+ }
513
+ if (sub === 'create') {
514
+ process.exitCode = await runAgentCreateCommand(args);
515
+ return;
516
+ }
517
+ if (sub === 'delete') {
518
+ process.exitCode = await runAgentDeleteCommand(args);
519
+ return;
520
+ }
521
+ console.error(`unknown agent subcommand: ${sub}`);
522
+ process.exit(1);
523
+ }
524
+
525
+ if (command === 'service') {
526
+ const sub = args._[1];
527
+ if (args.help || args.h || !sub) {
528
+ console.log(AGENT_COMMAND_HELP.service);
529
+ return;
530
+ }
531
+ if (sub === 'create') { process.exitCode = await runServiceCreateCommand(args); return; }
532
+ if (sub === 'update') { process.exitCode = await runServiceUpdateCommand(args); return; }
533
+ if (sub === 'delete') { process.exitCode = await runServiceDeleteCommand(args); return; }
534
+ if (sub === 'list') { process.exitCode = await runServiceListCommand(args); return; }
535
+ if (sub === 'info') { process.exitCode = await runServiceInfoCommand(args); return; }
536
+ if (sub === 'call') { process.exitCode = await runServiceCallCommand(args); return; }
537
+ console.error(`unknown service subcommand: ${sub}`);
538
+ process.exit(1);
539
+ }
540
+
541
+ if (command === 'credential') {
542
+ const sub = args._[1];
543
+ if (args.help || args.h || !sub) {
544
+ console.log(AGENT_COMMAND_HELP.credential);
545
+ return;
546
+ }
547
+ if (sub === 'request') {
548
+ process.exitCode = await runCredentialRequestCommand(args);
549
+ return;
550
+ }
551
+ console.error(`unknown credential subcommand: ${sub}`);
552
+ process.exit(1);
553
+ }
554
+
555
+ if (command === 'dashboard') {
556
+ const sub = args._[1];
557
+ if (args.help || args.h || !sub) {
558
+ console.log(AGENT_COMMAND_HELP.dashboard);
559
+ return;
560
+ }
561
+ if (sub === 'set') {
562
+ process.exitCode = await runDashboardSetCommand(args);
563
+ return;
564
+ }
565
+ if (sub === 'get') {
566
+ process.exitCode = await runDashboardGetCommand(args);
567
+ return;
568
+ }
569
+ console.error(`unknown dashboard subcommand: ${sub}`);
570
+ process.exit(1);
571
+ }
572
+
456
573
  if (command === 'group') {
457
574
  const sub = args._[1];
458
575
  if (args.help || args.h || !sub) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.16-dev.11",
3
+ "version": "0.1.16-dev.12",
4
4
  "description": "Local connector that links agent harnesses (Claude Code, Codex, OpenClaw, opencode, Pi) to the Ticlawk mobile app.",
5
5
  "type": "module",
6
6
  "main": "ticlawk.mjs",
@@ -188,6 +188,7 @@ export async function sendAgentMessage({
188
188
  runtimeHostId,
189
189
  visibility,
190
190
  mediaAssetIds,
191
+ metadata,
191
192
  }) {
192
193
  const { data } = await apiFetch('/api/agent/messages/send', {
193
194
  method: 'POST',
@@ -200,6 +201,7 @@ export async function sendAgentMessage({
200
201
  runtime_host_id: runtimeHostId ?? null,
201
202
  visibility: visibility || null,
202
203
  media_asset_ids: Array.isArray(mediaAssetIds) && mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
204
+ metadata: metadata ?? undefined,
203
205
  }),
204
206
  });
205
207
  return data || null;
@@ -484,6 +486,193 @@ export async function removeAgentGroupMember({
484
486
  );
485
487
  }
486
488
 
489
+ // ── Workstreams (CoS-managed groups) ──
490
+
491
+ export async function createWorkstream({
492
+ actingAgentId, name, description, charter, memberAgentIds,
493
+ }) {
494
+ return apiFetch('/api/agent/workstreams', {
495
+ method: 'POST',
496
+ body: JSON.stringify({
497
+ acting_as_agent_id: actingAgentId,
498
+ name,
499
+ description: description ?? null,
500
+ charter: charter ?? null,
501
+ member_agent_ids: memberAgentIds || [],
502
+ }),
503
+ });
504
+ }
505
+
506
+ export async function deleteWorkstream({ actingAgentId, conversationId }) {
507
+ return apiFetch(
508
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}`,
509
+ {
510
+ method: 'DELETE',
511
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
512
+ },
513
+ );
514
+ }
515
+
516
+ export async function listWorkstreams({ actingAgentId }) {
517
+ const params = new URLSearchParams();
518
+ params.set('acting_as_agent_id', actingAgentId);
519
+ const { data } = await apiFetch(`/api/agent/workstreams?${params}`);
520
+ return data || [];
521
+ }
522
+
523
+ // ── Agents (CoS pre-allocation) ──
524
+
525
+ export async function createAgentSlot({
526
+ actingAgentId, name, runtime, description, displayName, model,
527
+ }) {
528
+ return apiFetch('/api/agent/agents', {
529
+ method: 'POST',
530
+ body: JSON.stringify({
531
+ acting_as_agent_id: actingAgentId,
532
+ name,
533
+ runtime,
534
+ description: description ?? null,
535
+ display_name: displayName ?? null,
536
+ model: model ?? null,
537
+ }),
538
+ });
539
+ }
540
+
541
+ export async function archiveAgentSlot({ actingAgentId, agentId }) {
542
+ return apiFetch(
543
+ `/api/agent/agents/${encodeURIComponent(agentId)}`,
544
+ {
545
+ method: 'DELETE',
546
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
547
+ },
548
+ );
549
+ }
550
+
551
+ // ── Services ──
552
+
553
+ export async function createService({
554
+ actingAgentId, name, description, contractSchema, endpointConfig,
555
+ }) {
556
+ return apiFetch('/api/agent/services', {
557
+ method: 'POST',
558
+ body: JSON.stringify({
559
+ acting_as_agent_id: actingAgentId,
560
+ name,
561
+ description: description ?? null,
562
+ contract_schema: contractSchema ?? null,
563
+ endpoint_config: endpointConfig,
564
+ }),
565
+ });
566
+ }
567
+
568
+ export async function updateService({ actingAgentId, serviceId, ...patch }) {
569
+ return apiFetch(
570
+ `/api/agent/services/${encodeURIComponent(serviceId)}`,
571
+ {
572
+ method: 'PATCH',
573
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId, ...patch }),
574
+ },
575
+ );
576
+ }
577
+
578
+ export async function deleteService({ actingAgentId, serviceId }) {
579
+ return apiFetch(
580
+ `/api/agent/services/${encodeURIComponent(serviceId)}`,
581
+ {
582
+ method: 'DELETE',
583
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
584
+ },
585
+ );
586
+ }
587
+
588
+ export async function listServices({ actingAgentId }) {
589
+ const params = new URLSearchParams();
590
+ params.set('acting_as_agent_id', actingAgentId);
591
+ const { data } = await apiFetch(`/api/agent/services?${params}`);
592
+ return data || [];
593
+ }
594
+
595
+ export async function getServiceInfo({ actingAgentId, name }) {
596
+ const params = new URLSearchParams();
597
+ params.set('acting_as_agent_id', actingAgentId);
598
+ return apiFetch(
599
+ `/api/agent/services/${encodeURIComponent(name)}/info?${params}`,
600
+ );
601
+ }
602
+
603
+ export async function callService({ actingAgentId, name, input }) {
604
+ return apiFetch(
605
+ `/api/agent/services/${encodeURIComponent(name)}/call`,
606
+ {
607
+ method: 'POST',
608
+ body: JSON.stringify({ acting_as_agent_id: actingAgentId, input }),
609
+ },
610
+ );
611
+ }
612
+
613
+ // ── Credentials (CoS slot creation) ──
614
+
615
+ export async function requestCredential({
616
+ actingAgentId, name, description, workstreamId,
617
+ }) {
618
+ return apiFetch('/api/agent/credentials', {
619
+ method: 'POST',
620
+ body: JSON.stringify({
621
+ acting_as_agent_id: actingAgentId,
622
+ name,
623
+ description: description ?? null,
624
+ workstream_id: workstreamId ?? null,
625
+ }),
626
+ });
627
+ }
628
+
629
+ // ── Workstream dashboard ──
630
+
631
+ export async function setWorkstreamDashboard({
632
+ actingAgentId, conversationId, dataJson, htmlTemplate,
633
+ }) {
634
+ const body = { acting_as_agent_id: actingAgentId };
635
+ // Distinguish "omit" from "set to null" — only include keys the caller
636
+ // explicitly passed (including null clears the field).
637
+ if (dataJson !== undefined) body.data_json = dataJson;
638
+ if (htmlTemplate !== undefined) body.html_template = htmlTemplate;
639
+ return apiFetch(
640
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/dashboard`,
641
+ { method: 'POST', body: JSON.stringify(body) },
642
+ );
643
+ }
644
+
645
+ export async function getWorkstreamDashboard({ actingAgentId, conversationId }) {
646
+ const params = new URLSearchParams();
647
+ params.set('acting_as_agent_id', actingAgentId);
648
+ return apiFetch(
649
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/dashboard?${params}`,
650
+ );
651
+ }
652
+
653
+ // ── Workstream charter ──
654
+
655
+ export async function getWorkstreamCharter({ actingAgentId, conversationId }) {
656
+ const params = new URLSearchParams();
657
+ params.set('acting_as_agent_id', actingAgentId);
658
+ return apiFetch(
659
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/charter?${params}`,
660
+ );
661
+ }
662
+
663
+ export async function setWorkstreamCharter({ actingAgentId, conversationId, charter }) {
664
+ return apiFetch(
665
+ `/api/agent/workstreams/${encodeURIComponent(conversationId)}/charter`,
666
+ {
667
+ method: 'POST',
668
+ body: JSON.stringify({
669
+ acting_as_agent_id: actingAgentId,
670
+ charter: charter ?? null,
671
+ }),
672
+ },
673
+ );
674
+ }
675
+
487
676
  // ── Channel event pipe ──
488
677
 
489
678
  export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
@@ -140,14 +140,61 @@ function buildGroupContextBlock(msg) {
140
140
  ].join('\n');
141
141
  }
142
142
 
143
+ // Charter is the workstream's CoS-authored markdown spec. It's a stable
144
+ // prefix-cacheable block — same bytes across every turn in the same
145
+ // conversation — so it sits above the per-turn envelope.
146
+ function buildCharterBlock(msg) {
147
+ const charter = (msg.conversation_charter || '').trim();
148
+ if (!charter) return '';
149
+ return ['[charter]', charter, '[/charter]'].join('\n');
150
+ }
151
+
152
+ // CoS-only addendum. Splice above the per-turn envelope (after charter,
153
+ // before group context) when the recipient is the user's CoS.
154
+ // Same bytes every turn → still prefix-cache-friendly.
155
+ const COS_ADDENDUM = [
156
+ '[cos-role]',
157
+ 'You are the user\'s Chief of Staff (CoS). You answer to the user. In every workstream:',
158
+ '',
159
+ '1. Push — break goals into agent-executable tasks, supervise progress, do not let work stall in place.',
160
+ '2. Simplify — agents should work simply and reliably. Stop over-engineering, junk-code piles, and detours.',
161
+ '3. Track — keep an entry per workstream in MEMORY.md; update it whenever state changes.',
162
+ '4. Represent — bundle resource / decision / approval requests and bring them to the user; do not flood the user with raw asks.',
163
+ '',
164
+ 'Wake-on-event: every turn (including ambient delivery) you must read the message, update MEMORY.md if anything changed, and only reply when you need to intervene. Otherwise stop silently.',
165
+ '',
166
+ 'Posture: bring a proposal, default to acting on it, change course if the user objects. "I propose A — reasons 1/2/3. Unless you redirect within 24h, I\'ll proceed with A." Not "should we do A or B?"',
167
+ '',
168
+ 'Owner model: hold the user\'s preferences and constraints across workstreams. Apply them consistently. Do not "for-your-own-good" around stated preferences.',
169
+ '',
170
+ '奏折 / report posture: publish status to the user by sending a message into the CoS DM with `ticlawk message send --target dm:@<owner> --kind report` (stdin = the report body). Use this for scheduled summaries, escalations, or workstream check-ins. Reports surface in the Office → 奏折 sub-tab; chat replies do not. Do not over-spam reports — one per meaningful state change.',
171
+ '',
172
+ 'Dashboard posture: maintain a per-workstream dashboard via `ticlawk dashboard set --target "#<ws>"` (stdin JSON: { data_json, html_template }). data_json holds the live numbers; html_template is the hand-written rendering. CoS replaces wholesale — there is no partial update protocol, and there is no schema lock-in on data_json. Use the dashboard for at-a-glance state; use 奏折 for narrative.',
173
+ '',
174
+ 'Cadence: schedule your own daily 晨报 via `ticlawk reminder schedule --title "晨报" --in-minutes 1440 --anchor-conversation-id <your DM with owner>` after you handle this turn. The reminder fires by waking you with a system message; treat that wake as the trigger to compile the day\'s 奏折 across workstreams. Re-schedule on each fire so the cadence persists. Skip a day only if you are explicitly told to.',
175
+ '[/cos-role]',
176
+ ].join('\n');
177
+
178
+ function buildCosAddendum(msg) {
179
+ if (!msg.recipient_is_cos) return '';
180
+ return COS_ADDENDUM;
181
+ }
182
+
143
183
  // Wrap each per-turn message with an explicit reply instruction so the
144
184
  // runtime LLM never has to remember the standing prompt to figure out
145
185
  // HOW to reply. Codex in particular treats the developerInstructions as
146
186
  // background and ignores the chat-send pattern without this per-turn
147
187
  // nudge.
148
- function buildWakePromptText({ envelopeHeader, target, rawText, groupContext }) {
188
+ function buildWakePromptText({ envelopeHeader, target, rawText, groupContext, charterBlock, cosAddendum }) {
149
189
  const body = `${envelopeHeader} ${rawText || ''}`.trim();
150
- const lines = ['New message received:', '', body];
190
+ const lines = [];
191
+ if (charterBlock) {
192
+ lines.push(charterBlock, '');
193
+ }
194
+ if (cosAddendum) {
195
+ lines.push(cosAddendum, '');
196
+ }
197
+ lines.push('New message received:', '', body);
151
198
  if (groupContext) {
152
199
  lines.push('', groupContext);
153
200
  }
@@ -181,8 +228,10 @@ export function normalizeInboundMessage(msg) {
181
228
  const header = baseHeader + taskSuffix + reactionsSuffix;
182
229
  const target = buildEnvelopeTarget(enriched);
183
230
  const groupContext = buildGroupContextBlock(enriched);
231
+ const charterBlock = buildCharterBlock(enriched);
232
+ const cosAddendum = buildCosAddendum(enriched);
184
233
  const text = header
185
- ? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext })
234
+ ? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext, charterBlock, cosAddendum })
186
235
  : rawText;
187
236
  return {
188
237
  bindingId: recipientAgentId,
@@ -1013,7 +1062,17 @@ export function createTiclawkAdapter(ctx) {
1013
1062
 
1014
1063
  function connectWakeSocket() {
1015
1064
  connectorSocket = new TiclawkWakeClient({
1016
- getUrl: api.getConnectorWsUrl,
1065
+ // host_id rides on the WS query string so connector-wake can flip
1066
+ // runtime_hosts.online for the right row. Empty host_id is
1067
+ // tolerated by the server (it just skips the state write).
1068
+ getUrl: () => {
1069
+ const base = String(api.getConnectorWsUrl() || '').trim();
1070
+ if (!base) return '';
1071
+ const hostId = String(getHostId() || '').trim();
1072
+ if (!hostId) return base;
1073
+ const sep = base.includes('?') ? '&' : '?';
1074
+ return `${base}${sep}host_id=${encodeURIComponent(hostId)}`;
1075
+ },
1017
1076
  getApiKey: api.getApiKey,
1018
1077
  onEvent: handleWakeEvent,
1019
1078
  onStatus: handleWakeStatus,