ticlawk 0.1.16-dev.3 → 0.1.16-dev.30
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 +207 -25
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +232 -23
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +196 -195
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +607 -37
- package/src/core/agent-cli-handlers.mjs +449 -20
- package/src/core/agent-home.mjs +86 -10
- package/src/core/argv.mjs +11 -1
- package/src/core/http.mjs +126 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +101 -30
- package/src/migrate/write-initial-memory.mjs +5 -5
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
- package/src/runtimes/_shared/handbook/BASICS.md +27 -0
- package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +48 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
- package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
- package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
- package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
- package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
- package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
- package/src/runtimes/_shared/standing-prompt.mjs +111 -264
- package/src/runtimes/_shared/wake-prompt.mjs +261 -0
- package/src/runtimes/claude-code/index.mjs +30 -108
- package/src/runtimes/codex/index.mjs +114 -23
- package/src/runtimes/openclaw/index.mjs +16 -26
- package/src/runtimes/opencode/index.mjs +42 -36
- package/src/runtimes/opencode/session.mjs +5 -4
- package/src/runtimes/pi/index.mjs +39 -31
- package/src/runtimes/pi/session.mjs +5 -2
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* and forwards to the ticlawk backend using the connector API key.
|
|
8
8
|
*
|
|
9
9
|
* Targets are parsed in the daemon (not on the wire to backend) so the
|
|
10
|
-
* CLI can speak `#<group>` / `dm:@<user>` /
|
|
10
|
+
* CLI can speak `#<group>` / `dm:<conversation-id>` / `dm:@<user>` /
|
|
11
|
+
* `#<group>:<short-msg-id>`
|
|
11
12
|
* while backend keeps a flat conversation_id contract.
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -34,7 +35,7 @@ export function invalidateServerInfoCache(actingAgentId = null) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* Parse a target string into { conversationId,
|
|
38
|
+
* Parse a target string into { conversationId, replyToMessageId } using a
|
|
38
39
|
* cached server-info lookup. Returns null fields if the target cannot be
|
|
39
40
|
* resolved; callers should treat that as a 404.
|
|
40
41
|
*
|
|
@@ -43,29 +44,29 @@ export function invalidateServerInfoCache(actingAgentId = null) {
|
|
|
43
44
|
* dm:@<handle> -> find DM conversation whose other member is <handle>
|
|
44
45
|
* #<uuid> -> conversation_id = <uuid>
|
|
45
46
|
* #<group-name> -> find group conversation by name
|
|
46
|
-
* <foo>:<short-msg-id> ->
|
|
47
|
+
* <foo>:<short-msg-id> -> replies under <foo>, root = first message whose
|
|
47
48
|
* id startsWith <short-msg-id>
|
|
48
49
|
*/
|
|
49
50
|
export async function resolveTarget(actingAgentId, target) {
|
|
50
|
-
if (!target) return { conversationId: null,
|
|
51
|
+
if (!target) return { conversationId: null, replyToMessageId: null, error: 'target is required' };
|
|
51
52
|
|
|
52
|
-
// Strip optional
|
|
53
|
+
// Strip optional message-reply suffix.
|
|
53
54
|
let base = target;
|
|
54
|
-
let
|
|
55
|
+
let replyToMessageId = null;
|
|
55
56
|
const colonIdx = target.indexOf(':', target.startsWith('dm:') ? 3 : 1);
|
|
56
57
|
if (colonIdx > 0 && colonIdx < target.length - 1) {
|
|
57
|
-
|
|
58
|
+
replyToMessageId = target.slice(colonIdx + 1);
|
|
58
59
|
base = target.slice(0, colonIdx);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
if (/^[0-9a-f-]{36}$/i.test(base)) {
|
|
62
|
-
return { conversationId: base,
|
|
63
|
+
return { conversationId: base, replyToMessageId, error: null };
|
|
63
64
|
}
|
|
64
65
|
if (base.startsWith('dm:') && /^[0-9a-f-]{36}$/i.test(base.slice(3))) {
|
|
65
|
-
return { conversationId: base.slice(3),
|
|
66
|
+
return { conversationId: base.slice(3), replyToMessageId, error: null };
|
|
66
67
|
}
|
|
67
68
|
if (base.startsWith('#') && /^[0-9a-f-]{36}$/i.test(base.slice(1))) {
|
|
68
|
-
return { conversationId: base.slice(1),
|
|
69
|
+
return { conversationId: base.slice(1), replyToMessageId, error: null };
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
const info = await getCachedServerInfo(actingAgentId);
|
|
@@ -76,19 +77,30 @@ export async function resolveTarget(actingAgentId, target) {
|
|
|
76
77
|
const match = convs.find((c) =>
|
|
77
78
|
c.type === 'dm' && (String(c.display_name || c.name || '').toLowerCase() === handle)
|
|
78
79
|
);
|
|
79
|
-
if (match) return { conversationId: match.id,
|
|
80
|
-
return { conversationId: null,
|
|
80
|
+
if (match) return { conversationId: match.id, replyToMessageId, error: null };
|
|
81
|
+
return { conversationId: null, replyToMessageId: null, error: `unknown dm target: ${target}` };
|
|
81
82
|
}
|
|
82
83
|
if (base.startsWith('#')) {
|
|
83
84
|
const name = base.slice(1).toLowerCase();
|
|
84
85
|
const match = convs.find((c) =>
|
|
85
86
|
c.type === 'group' && (String(c.name || c.display_name || '').toLowerCase() === name)
|
|
86
87
|
);
|
|
87
|
-
if (match) return { conversationId: match.id,
|
|
88
|
-
|
|
88
|
+
if (match) return { conversationId: match.id, replyToMessageId, error: null };
|
|
89
|
+
|
|
90
|
+
if (info?.agent?.is_cos) {
|
|
91
|
+
const workstreams = await api.listWorkstreams({ actingAgentId });
|
|
92
|
+
const workstreamMatch = workstreams.find((c) =>
|
|
93
|
+
String(c.name || c.display_name || '').toLowerCase() === name
|
|
94
|
+
);
|
|
95
|
+
if (workstreamMatch) {
|
|
96
|
+
return { conversationId: workstreamMatch.id, replyToMessageId, error: null };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { conversationId: null, replyToMessageId: null, error: `unknown group target: ${target}` };
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
return { conversationId: null,
|
|
103
|
+
return { conversationId: null, replyToMessageId: null, error: `invalid target syntax: ${target}` };
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
function getActingAgentId(req, body = {}) {
|
|
@@ -109,6 +121,15 @@ function getRuntimeHostId(req, body = {}) {
|
|
|
109
121
|
return null;
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
function getCurrentConversationId(req, body = {}) {
|
|
125
|
+
const fromHeader = req.headers['x-ticlawk-current-conversation-id'];
|
|
126
|
+
if (typeof fromHeader === 'string' && fromHeader.trim()) return fromHeader.trim();
|
|
127
|
+
if (typeof body?.current_conversation_id === 'string' && body.current_conversation_id.trim()) {
|
|
128
|
+
return body.current_conversation_id.trim();
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
function validateActingAgent(actingAgentId, ctx) {
|
|
113
134
|
if (!actingAgentId) {
|
|
114
135
|
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,27 +153,52 @@ export async function handleMessageSend(req, body, ctx) {
|
|
|
132
153
|
if (!text) return { status: 400, body: { error: 'text is required' } };
|
|
133
154
|
|
|
134
155
|
let conversationId = body?.conversation_id || null;
|
|
135
|
-
let
|
|
156
|
+
let targetReplyToMessageId = null;
|
|
136
157
|
if (!conversationId && body?.target) {
|
|
137
158
|
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
138
159
|
if (resolved.error) {
|
|
139
160
|
return { status: 404, body: { error: resolved.error } };
|
|
140
161
|
}
|
|
141
162
|
conversationId = resolved.conversationId;
|
|
142
|
-
|
|
163
|
+
targetReplyToMessageId = resolved.replyToMessageId;
|
|
143
164
|
}
|
|
144
165
|
if (!conversationId) {
|
|
145
166
|
return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
146
167
|
}
|
|
147
168
|
|
|
169
|
+
const currentConversationId = getCurrentConversationId(req, body);
|
|
170
|
+
if (currentConversationId && currentConversationId !== conversationId && !body?.allow_cross_target) {
|
|
171
|
+
debugLog('agent-cli', 'send.blocked-cross-target', {
|
|
172
|
+
actingAgentId,
|
|
173
|
+
currentConversationId,
|
|
174
|
+
conversationId,
|
|
175
|
+
target: body?.target || null,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
status: 409,
|
|
179
|
+
body: {
|
|
180
|
+
error: 'refusing to send to a different conversation from the current runtime turn',
|
|
181
|
+
current_conversation_id: currentConversationId,
|
|
182
|
+
target_conversation_id: conversationId,
|
|
183
|
+
hint: 'Use --allow-cross-target only for an intentional cross-conversation send.',
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const mediaAssetIds = Array.isArray(body?.media_asset_ids)
|
|
189
|
+
? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
|
|
190
|
+
: [];
|
|
191
|
+
|
|
148
192
|
try {
|
|
149
193
|
const data = await api.sendAgentMessage({
|
|
150
194
|
actingAgentId,
|
|
151
195
|
conversationId,
|
|
152
196
|
text,
|
|
153
197
|
seenUpToSeq: body?.seen_up_to_seq,
|
|
154
|
-
replyToMessageId: body?.reply_to_message_id ||
|
|
198
|
+
replyToMessageId: body?.reply_to_message_id || targetReplyToMessageId || null,
|
|
155
199
|
runtimeHostId: getRuntimeHostId(req, body),
|
|
200
|
+
mediaAssetIds,
|
|
201
|
+
metadata: body?.metadata,
|
|
156
202
|
});
|
|
157
203
|
debugLog('agent-cli', 'send.ok', {
|
|
158
204
|
actingAgentId,
|
|
@@ -224,6 +270,7 @@ export async function handleTaskCreate(req, body, ctx) {
|
|
|
224
270
|
conversationId,
|
|
225
271
|
text,
|
|
226
272
|
title: body?.title ?? null,
|
|
273
|
+
assignAgentId: body?.assign_agent_id || body?.assignee_agent_id || null,
|
|
227
274
|
});
|
|
228
275
|
debugLog('agent-cli', 'task.create', {
|
|
229
276
|
actingAgentId,
|
|
@@ -587,8 +634,8 @@ export async function handleAttachmentUpload(req, body, ctx) {
|
|
|
587
634
|
});
|
|
588
635
|
debugLog('agent-cli', 'attachment.upload', {
|
|
589
636
|
actingAgentId,
|
|
590
|
-
asset_id: data?.
|
|
591
|
-
bytes: data?.
|
|
637
|
+
asset_id: data?.data?.asset_id,
|
|
638
|
+
bytes: data?.data?.size_bytes,
|
|
592
639
|
});
|
|
593
640
|
return { status: 200, body: data };
|
|
594
641
|
} catch (err) {
|
|
@@ -596,6 +643,28 @@ export async function handleAttachmentUpload(req, body, ctx) {
|
|
|
596
643
|
}
|
|
597
644
|
}
|
|
598
645
|
|
|
646
|
+
export async function handleProfileAvatarUpload(req, body, ctx) {
|
|
647
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
648
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
649
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
650
|
+
if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
|
|
651
|
+
if (!body?.content_type || !String(body.content_type).startsWith('image/')) {
|
|
652
|
+
return { status: 400, body: { error: 'content_type must be image/*' } };
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const data = await api.uploadAgentAvatar({
|
|
656
|
+
actingAgentId,
|
|
657
|
+
filename: body?.filename || 'avatar.bin',
|
|
658
|
+
contentType: body.content_type,
|
|
659
|
+
dataBase64: body.data_base64,
|
|
660
|
+
});
|
|
661
|
+
debugLog('agent-cli', 'avatar.upload', { actingAgentId, url: data?.url });
|
|
662
|
+
return { status: 200, body: data };
|
|
663
|
+
} catch (err) {
|
|
664
|
+
return { status: err?.status || 500, body: { error: err?.message || 'avatar upload failed' } };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
599
668
|
export async function handleAttachmentView(req, query, ctx) {
|
|
600
669
|
const actingAgentId = getActingAgentId(req, query);
|
|
601
670
|
const v = validateActingAgent(actingAgentId, ctx);
|
|
@@ -689,6 +758,366 @@ export async function handleGroupMembersRemove(req, body, ctx) {
|
|
|
689
758
|
}
|
|
690
759
|
}
|
|
691
760
|
|
|
761
|
+
export async function handleWorkstreamCreate(req, body, ctx) {
|
|
762
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
763
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
764
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
765
|
+
const name = String(body?.name || '').trim();
|
|
766
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
767
|
+
try {
|
|
768
|
+
const data = await api.createWorkstream({
|
|
769
|
+
actingAgentId,
|
|
770
|
+
name,
|
|
771
|
+
description: body?.description || null,
|
|
772
|
+
charter: body?.charter ?? null,
|
|
773
|
+
memberAgentIds: Array.isArray(body?.member_agent_ids) ? body.member_agent_ids : [],
|
|
774
|
+
});
|
|
775
|
+
invalidateServerInfoCache(actingAgentId);
|
|
776
|
+
debugLog('agent-cli', 'workstream.create', {
|
|
777
|
+
actingAgentId, conversationId: data?.conversation?.id,
|
|
778
|
+
});
|
|
779
|
+
return { status: 200, body: data };
|
|
780
|
+
} catch (err) {
|
|
781
|
+
return { status: err?.status || 500, body: { error: err?.message || 'workstream create failed' } };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function handleWorkstreamDelete(req, body, ctx) {
|
|
786
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
787
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
788
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
789
|
+
let conversationId = body?.conversation_id || null;
|
|
790
|
+
if (!conversationId && body?.target) {
|
|
791
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
792
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
793
|
+
conversationId = resolved.conversationId;
|
|
794
|
+
}
|
|
795
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
796
|
+
try {
|
|
797
|
+
const data = await api.deleteWorkstream({ actingAgentId, conversationId });
|
|
798
|
+
invalidateServerInfoCache(actingAgentId);
|
|
799
|
+
return { status: 200, body: data };
|
|
800
|
+
} catch (err) {
|
|
801
|
+
return { status: err?.status || 500, body: { error: err?.message || 'workstream delete failed' } };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export async function handleWorkstreamList(req, query, ctx) {
|
|
806
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
807
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
808
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
809
|
+
try {
|
|
810
|
+
const data = await api.listWorkstreams({ actingAgentId });
|
|
811
|
+
return { status: 200, body: { data } };
|
|
812
|
+
} catch (err) {
|
|
813
|
+
return { status: err?.status || 500, body: { error: err?.message || 'workstream list failed' } };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export async function handleAgentList(req, query, ctx) {
|
|
818
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
819
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
820
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
821
|
+
try {
|
|
822
|
+
const data = await api.listAgentSlots({ actingAgentId });
|
|
823
|
+
return { status: 200, body: { data } };
|
|
824
|
+
} catch (err) {
|
|
825
|
+
return { status: err?.status || 500, body: { error: err?.message || 'agent list failed' } };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export async function handleAgentCreate(req, body, ctx) {
|
|
830
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
831
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
832
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
833
|
+
const name = String(body?.name || '').trim();
|
|
834
|
+
const runtime = String(body?.runtime || '').trim();
|
|
835
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
836
|
+
if (!runtime) return { status: 400, body: { error: 'runtime is required' } };
|
|
837
|
+
try {
|
|
838
|
+
const data = await api.createAgentSlot({
|
|
839
|
+
actingAgentId,
|
|
840
|
+
name,
|
|
841
|
+
runtime,
|
|
842
|
+
description: body?.description || null,
|
|
843
|
+
displayName: body?.display_name || null,
|
|
844
|
+
model: body?.model || null,
|
|
845
|
+
});
|
|
846
|
+
return { status: 200, body: data };
|
|
847
|
+
} catch (err) {
|
|
848
|
+
return { status: err?.status || 500, body: { error: err?.message || 'agent create failed' } };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export async function handleAgentDelete(req, body, ctx) {
|
|
853
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
854
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
855
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
856
|
+
const agentId = String(body?.agent_id || '').trim();
|
|
857
|
+
if (!agentId) return { status: 400, body: { error: 'agent_id is required' } };
|
|
858
|
+
try {
|
|
859
|
+
const data = await api.archiveAgentSlot({ actingAgentId, agentId });
|
|
860
|
+
return { status: 200, body: data };
|
|
861
|
+
} catch (err) {
|
|
862
|
+
return { status: err?.status || 500, body: { error: err?.message || 'agent delete failed' } };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export async function handleServiceCreate(req, body, ctx) {
|
|
867
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
868
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
869
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
870
|
+
const name = String(body?.name || '').trim();
|
|
871
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
872
|
+
if (!body?.endpoint_config || typeof body.endpoint_config !== 'object') {
|
|
873
|
+
return { status: 400, body: { error: 'endpoint_config is required' } };
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
const data = await api.createService({
|
|
877
|
+
actingAgentId, name,
|
|
878
|
+
description: body?.description || null,
|
|
879
|
+
contractSchema: body?.contract_schema ?? null,
|
|
880
|
+
endpointConfig: body.endpoint_config,
|
|
881
|
+
});
|
|
882
|
+
return { status: 200, body: data };
|
|
883
|
+
} catch (err) {
|
|
884
|
+
return { status: err?.status || 500, body: { error: err?.message || 'service create failed' } };
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export async function handleServiceUpdate(req, body, ctx) {
|
|
889
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
890
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
891
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
892
|
+
const serviceId = String(body?.service_id || '').trim();
|
|
893
|
+
if (!serviceId) return { status: 400, body: { error: 'service_id is required' } };
|
|
894
|
+
const patch = {};
|
|
895
|
+
if ('description' in (body || {})) patch.description = body.description;
|
|
896
|
+
if ('contract_schema' in (body || {})) patch.contract_schema = body.contract_schema;
|
|
897
|
+
if ('endpoint_config' in (body || {})) patch.endpoint_config = body.endpoint_config;
|
|
898
|
+
if ('status' in (body || {})) patch.status = body.status;
|
|
899
|
+
try {
|
|
900
|
+
const data = await api.updateService({ actingAgentId, serviceId, ...patch });
|
|
901
|
+
return { status: 200, body: data };
|
|
902
|
+
} catch (err) {
|
|
903
|
+
return { status: err?.status || 500, body: { error: err?.message || 'service update failed' } };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export async function handleServiceDelete(req, body, ctx) {
|
|
908
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
909
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
910
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
911
|
+
const serviceId = String(body?.service_id || '').trim();
|
|
912
|
+
if (!serviceId) return { status: 400, body: { error: 'service_id is required' } };
|
|
913
|
+
try {
|
|
914
|
+
const data = await api.deleteService({ actingAgentId, serviceId });
|
|
915
|
+
return { status: 200, body: data };
|
|
916
|
+
} catch (err) {
|
|
917
|
+
return { status: err?.status || 500, body: { error: err?.message || 'service delete failed' } };
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export async function handleServiceList(req, query, ctx) {
|
|
922
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
923
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
924
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
925
|
+
try {
|
|
926
|
+
const data = await api.listServices({ actingAgentId });
|
|
927
|
+
return { status: 200, body: { data } };
|
|
928
|
+
} catch (err) {
|
|
929
|
+
return { status: err?.status || 500, body: { error: err?.message || 'service list failed' } };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export async function handleServiceInfo(req, query, ctx) {
|
|
934
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
935
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
936
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
937
|
+
const name = String(query?.name || '').trim();
|
|
938
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
939
|
+
try {
|
|
940
|
+
const data = await api.getServiceInfo({ actingAgentId, name });
|
|
941
|
+
return { status: 200, body: data };
|
|
942
|
+
} catch (err) {
|
|
943
|
+
return { status: err?.status || 500, body: { error: err?.message || 'service info failed' } };
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export async function handleServiceCall(req, body, ctx) {
|
|
948
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
949
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
950
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
951
|
+
const name = String(body?.name || '').trim();
|
|
952
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
953
|
+
try {
|
|
954
|
+
const data = await api.callService({
|
|
955
|
+
actingAgentId, name,
|
|
956
|
+
input: body?.input ?? null,
|
|
957
|
+
});
|
|
958
|
+
return { status: 200, body: data };
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return { status: err?.status || 500, body: { error: err?.message || 'service call failed' } };
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export async function handleBriefingGet(req, query, ctx) {
|
|
965
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
966
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
967
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
968
|
+
const briefingId = String(query?.id || '').trim();
|
|
969
|
+
if (!briefingId) return { status: 400, body: { error: 'id is required' } };
|
|
970
|
+
try {
|
|
971
|
+
const data = await api.getBriefing({ actingAgentId, briefingId });
|
|
972
|
+
return { status: 200, body: data };
|
|
973
|
+
} catch (err) {
|
|
974
|
+
return { status: err?.status || 500, body: { error: err?.message || 'briefing get failed' } };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export async function handleBriefingPublish(req, body, ctx) {
|
|
979
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
980
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
981
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
982
|
+
const bodyText = typeof body?.body_text === 'string' && body.body_text.trim() ? body.body_text.trim() : null;
|
|
983
|
+
const attachmentAssetId = typeof body?.attachment_asset_id === 'string' && body.attachment_asset_id.trim()
|
|
984
|
+
? body.attachment_asset_id.trim()
|
|
985
|
+
: null;
|
|
986
|
+
const responseMode = typeof body?.response_mode === 'string' && body.response_mode.trim()
|
|
987
|
+
? body.response_mode.trim().toLowerCase()
|
|
988
|
+
: 'info';
|
|
989
|
+
if (!bodyText) {
|
|
990
|
+
return { status: 400, body: { error: 'body_text is required' } };
|
|
991
|
+
}
|
|
992
|
+
if (bodyText && bodyText.length > 140) {
|
|
993
|
+
return { status: 400, body: { error: 'body_text must be ≤140 chars' } };
|
|
994
|
+
}
|
|
995
|
+
if (!['info', 'approval'].includes(responseMode)) {
|
|
996
|
+
return { status: 400, body: { error: 'response_mode must be info or approval' } };
|
|
997
|
+
}
|
|
998
|
+
const currentConversationId = getCurrentConversationId(req, body);
|
|
999
|
+
try {
|
|
1000
|
+
const data = await api.publishBriefing({
|
|
1001
|
+
actingAgentId,
|
|
1002
|
+
bodyText,
|
|
1003
|
+
attachmentAssetId,
|
|
1004
|
+
currentConversationId,
|
|
1005
|
+
responseMode,
|
|
1006
|
+
});
|
|
1007
|
+
return { status: 200, body: data };
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
return { status: err?.status || 500, body: { error: err?.message || 'briefing publish failed' } };
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export async function handleCredentialRequest(req, body, ctx) {
|
|
1014
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
1015
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
1016
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
1017
|
+
const name = String(body?.name || '').trim();
|
|
1018
|
+
if (!name) return { status: 400, body: { error: 'name is required' } };
|
|
1019
|
+
let workstreamId = body?.workstream_id || null;
|
|
1020
|
+
if (!workstreamId && body?.target) {
|
|
1021
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
1022
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
1023
|
+
workstreamId = resolved.conversationId;
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
const data = await api.requestCredential({
|
|
1027
|
+
actingAgentId,
|
|
1028
|
+
name,
|
|
1029
|
+
description: body?.description || null,
|
|
1030
|
+
workstreamId,
|
|
1031
|
+
});
|
|
1032
|
+
return { status: 200, body: data };
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
return { status: err?.status || 500, body: { error: err?.message || 'credential request failed' } };
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
export async function handleWorkstreamDashboardSet(req, body, ctx) {
|
|
1039
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
1040
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
1041
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
1042
|
+
let conversationId = body?.conversation_id || null;
|
|
1043
|
+
if (!conversationId && body?.target) {
|
|
1044
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
1045
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
1046
|
+
conversationId = resolved.conversationId;
|
|
1047
|
+
}
|
|
1048
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
1049
|
+
const payload = { actingAgentId, conversationId };
|
|
1050
|
+
if ('data_json' in (body || {})) payload.dataJson = body.data_json;
|
|
1051
|
+
if ('html_template' in (body || {})) payload.htmlTemplate = body.html_template;
|
|
1052
|
+
try {
|
|
1053
|
+
const data = await api.setWorkstreamDashboard(payload);
|
|
1054
|
+
return { status: 200, body: data };
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
return { status: err?.status || 500, body: { error: err?.message || 'dashboard set failed' } };
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export async function handleWorkstreamDashboardGet(req, query, ctx) {
|
|
1061
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
1062
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
1063
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
1064
|
+
let conversationId = query?.conversation_id || null;
|
|
1065
|
+
if (!conversationId && query?.target) {
|
|
1066
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
1067
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
1068
|
+
conversationId = resolved.conversationId;
|
|
1069
|
+
}
|
|
1070
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
1071
|
+
try {
|
|
1072
|
+
const data = await api.getWorkstreamDashboard({ actingAgentId, conversationId });
|
|
1073
|
+
return { status: 200, body: data };
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
return { status: err?.status || 500, body: { error: err?.message || 'dashboard get failed' } };
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export async function handleWorkstreamCharterGet(req, query, ctx) {
|
|
1080
|
+
const actingAgentId = getActingAgentId(req, query);
|
|
1081
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
1082
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
1083
|
+
let conversationId = query?.conversation_id || null;
|
|
1084
|
+
if (!conversationId && query?.target) {
|
|
1085
|
+
const resolved = await resolveTarget(actingAgentId, String(query.target));
|
|
1086
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
1087
|
+
conversationId = resolved.conversationId;
|
|
1088
|
+
}
|
|
1089
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
1090
|
+
try {
|
|
1091
|
+
const data = await api.getWorkstreamCharter({ actingAgentId, conversationId });
|
|
1092
|
+
return { status: 200, body: data };
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
return { status: err?.status || 500, body: { error: err?.message || 'charter get failed' } };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export async function handleWorkstreamCharterSet(req, body, ctx) {
|
|
1099
|
+
const actingAgentId = getActingAgentId(req, body);
|
|
1100
|
+
const v = validateActingAgent(actingAgentId, ctx);
|
|
1101
|
+
if (!v.ok) return { status: v.status, body: { error: v.error } };
|
|
1102
|
+
let conversationId = body?.conversation_id || null;
|
|
1103
|
+
if (!conversationId && body?.target) {
|
|
1104
|
+
const resolved = await resolveTarget(actingAgentId, String(body.target));
|
|
1105
|
+
if (resolved.error) return { status: 404, body: { error: resolved.error } };
|
|
1106
|
+
conversationId = resolved.conversationId;
|
|
1107
|
+
}
|
|
1108
|
+
if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
|
|
1109
|
+
const charter = typeof body?.charter === 'string' ? body.charter : null;
|
|
1110
|
+
try {
|
|
1111
|
+
const data = await api.setWorkstreamCharter({ actingAgentId, conversationId, charter });
|
|
1112
|
+
debugLog('agent-cli', 'workstream.charter.set', {
|
|
1113
|
+
actingAgentId, conversationId, len: charter?.length ?? 0,
|
|
1114
|
+
});
|
|
1115
|
+
return { status: 200, body: data };
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
return { status: err?.status || 500, body: { error: err?.message || 'charter set failed' } };
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
692
1121
|
export async function handleServerInfo(req, query, ctx) {
|
|
693
1122
|
const actingAgentId = getActingAgentId(req, query);
|
|
694
1123
|
const v = validateActingAgent(actingAgentId, ctx);
|
package/src/core/agent-home.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-agent
|
|
2
|
+
* Per-agent home directory: ~/.ticlawk/agents/<agent_id>/
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Following slock's daemon model (dist/chunk-M4A5QPUN.js:5079) —
|
|
9
|
-
* dataDir = ~/.slock/agents, agentDataDir = <dataDir>/<id>, and every
|
|
10
|
-
* driver.spawn() receives `workingDirectory: agentDataDir`. One MEMORY.md
|
|
11
|
-
* per agent, lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
4
|
+
* The agent's authoritative workspace. The daemon spawns every runtime
|
|
5
|
+
* with this as cwd; the agent's MEMORY.md, notes/, and any artifacts
|
|
6
|
+
* it produces live here. No project binding — one MEMORY.md per agent,
|
|
7
|
+
* lives in cwd, agent reads it via `cat MEMORY.md`.
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
15
11
|
import { join } from 'node:path';
|
|
16
12
|
import { AF_HOME } from './config.mjs';
|
|
13
|
+
import { buildAgentHandbookFiles, LEGACY_HANDBOOK_FILE_NAMES } from '../runtimes/_shared/agent-handbook.mjs';
|
|
17
14
|
|
|
18
15
|
export const AF_AGENTS_DIR = join(AF_HOME, 'agents');
|
|
19
16
|
|
|
@@ -41,9 +38,88 @@ export function ensureAgentHome(agentId, { displayName } = {}) {
|
|
|
41
38
|
if (!existsSync(memoryPath)) {
|
|
42
39
|
writeFileSync(memoryPath, buildInitialMemoryMd({ displayName, home }), 'utf8');
|
|
43
40
|
}
|
|
41
|
+
writeManagedHandbookFiles(home);
|
|
42
|
+
ensureSkillSymlinks(home);
|
|
44
43
|
return home;
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function writeManagedHandbookFiles(home) {
|
|
47
|
+
removeLegacyManagedHandbookFiles(home);
|
|
48
|
+
for (const { name, content } of buildAgentHandbookFiles()) {
|
|
49
|
+
const path = join(home, name);
|
|
50
|
+
try {
|
|
51
|
+
let stat = null;
|
|
52
|
+
try { stat = lstatSync(path); } catch { /* not present */ }
|
|
53
|
+
if (stat && !stat.isFile()) continue;
|
|
54
|
+
const next = `${content.trim()}\n`;
|
|
55
|
+
if (stat) {
|
|
56
|
+
const current = readFileSync(path, 'utf8');
|
|
57
|
+
if (current === next) continue;
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(path, next, { encoding: 'utf8', mode: 0o600 });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn(`[agent-home] failed to write ${path}: ${err?.message || err}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function removeLegacyManagedHandbookFiles(home) {
|
|
67
|
+
for (const name of LEGACY_HANDBOOK_FILE_NAMES) {
|
|
68
|
+
const path = join(home, name);
|
|
69
|
+
try {
|
|
70
|
+
const stat = lstatSync(path);
|
|
71
|
+
if (!stat.isFile()) continue;
|
|
72
|
+
unlinkSync(path);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err?.code === 'ENOENT') continue;
|
|
75
|
+
console.warn(`[agent-home] failed to remove legacy handbook ${path}: ${err?.message || err}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cross-runtime skill discovery: every runtime (Claude Code, Codex,
|
|
82
|
+
* opencode, pi, openclaw) auto-discovers SKILL.md under at least one of
|
|
83
|
+
* .claude/skills/, .codex/skills/, .opencode/skills/, .pi/skills/, or
|
|
84
|
+
* .agents/skills/. We pin everything to a single source-of-truth
|
|
85
|
+
* directory (.agents/skills/) and symlink the rest so a skill written
|
|
86
|
+
* once is visible to every runtime without sync machinery.
|
|
87
|
+
*
|
|
88
|
+
* .pi and .opencode skip the symlink — both natively scan
|
|
89
|
+
* .agents/skills/ in addition to their own folder.
|
|
90
|
+
*
|
|
91
|
+
* No migration / no recovery: if the target path already exists as a
|
|
92
|
+
* real dir or wrong-target symlink, we leave it alone. The user (or a
|
|
93
|
+
* future agent) resolves it manually. See cos_impl.md §一.不为错误兜底.
|
|
94
|
+
*/
|
|
95
|
+
function ensureSkillSymlinks(home) {
|
|
96
|
+
const realRoot = join(home, '.agents', 'skills');
|
|
97
|
+
mkdirSync(realRoot, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const links = [
|
|
100
|
+
{ dir: join(home, '.claude'), name: 'skills', target: '../.agents/skills' },
|
|
101
|
+
{ dir: join(home, '.codex'), name: 'skills', target: '../.agents/skills' },
|
|
102
|
+
// openclaw expects skills/ at the home root
|
|
103
|
+
{ dir: home, name: 'skills', target: '.agents/skills' },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const { dir, name, target } of links) {
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
const linkPath = join(dir, name);
|
|
109
|
+
let stat = null;
|
|
110
|
+
try { stat = lstatSync(linkPath); } catch { /* not present */ }
|
|
111
|
+
if (stat) continue; // path already exists — do not touch
|
|
112
|
+
try {
|
|
113
|
+
symlinkSync(target, linkPath, 'dir');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// Best-effort: an EEXIST race or platform that disallows symlinks
|
|
116
|
+
// gets logged but doesn't fail spawn. Skills just won't be visible
|
|
117
|
+
// for that runtime via this folder.
|
|
118
|
+
console.warn(`[agent-home] failed to symlink ${linkPath} -> ${target}: ${err?.message || err}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
47
123
|
function buildInitialMemoryMd({ displayName, home }) {
|
|
48
124
|
const lines = [
|
|
49
125
|
`# ${displayName || 'Agent'}`,
|