ticlawk 0.1.16-dev.9 → 0.1.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 +15 -3
- package/bin/ticlawk.mjs +208 -21
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +283 -48
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +126 -121
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +557 -18
- package/src/core/agent-cli-handlers.mjs +435 -18
- package/src/core/agent-home.mjs +81 -1
- package/src/core/argv.mjs +11 -1
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/http.mjs +119 -0
- package/src/core/runtime-contract.mjs +0 -1
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +108 -77
- 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 +55 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +46 -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 +134 -278
- package/src/runtimes/_shared/wake-prompt.mjs +261 -0
- package/src/runtimes/claude-code/index.mjs +19 -46
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +115 -63
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/openclaw/index.mjs +11 -24
- package/src/runtimes/opencode/index.mjs +38 -60
- package/src/runtimes/opencode/session.mjs +12 -12
- package/src/runtimes/pi/index.mjs +38 -60
- package/src/runtimes/pi/session.mjs +9 -6
- package/ticlawk.mjs +0 -30
|
@@ -30,8 +30,9 @@ export function getConnectorWsUrl() {
|
|
|
30
30
|
return DEFAULT_CONNECTOR_WS_URL;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
33
|
+
// Non-terminal event writes are serialized per agent for stable logs.
|
|
34
|
+
// Terminal turn events bypass this queue so telemetry cannot hold up the
|
|
35
|
+
// delivery lifecycle.
|
|
35
36
|
const agentEventQueues = new Map(); // agentId -> Promise
|
|
36
37
|
|
|
37
38
|
export class TiclawkUpdateRequiredError extends Error {
|
|
@@ -188,6 +189,7 @@ export async function sendAgentMessage({
|
|
|
188
189
|
runtimeHostId,
|
|
189
190
|
visibility,
|
|
190
191
|
mediaAssetIds,
|
|
192
|
+
metadata,
|
|
191
193
|
}) {
|
|
192
194
|
const { data } = await apiFetch('/api/agent/messages/send', {
|
|
193
195
|
method: 'POST',
|
|
@@ -200,6 +202,7 @@ export async function sendAgentMessage({
|
|
|
200
202
|
runtime_host_id: runtimeHostId ?? null,
|
|
201
203
|
visibility: visibility || null,
|
|
202
204
|
media_asset_ids: Array.isArray(mediaAssetIds) && mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
|
|
205
|
+
metadata: metadata ?? undefined,
|
|
203
206
|
}),
|
|
204
207
|
});
|
|
205
208
|
return data || null;
|
|
@@ -222,7 +225,7 @@ export async function readAgentMessages({
|
|
|
222
225
|
return data || [];
|
|
223
226
|
}
|
|
224
227
|
|
|
225
|
-
export async function createAgentTask({ actingAgentId, conversationId, text, title }) {
|
|
228
|
+
export async function createAgentTask({ actingAgentId, conversationId, text, title, assignAgentId }) {
|
|
226
229
|
return apiFetch('/api/agent/tasks/create', {
|
|
227
230
|
method: 'POST',
|
|
228
231
|
body: JSON.stringify({
|
|
@@ -230,6 +233,7 @@ export async function createAgentTask({ actingAgentId, conversationId, text, tit
|
|
|
230
233
|
conversation_id: conversationId,
|
|
231
234
|
text,
|
|
232
235
|
title: title ?? null,
|
|
236
|
+
assign_agent_id: assignAgentId ?? null,
|
|
233
237
|
}),
|
|
234
238
|
});
|
|
235
239
|
}
|
|
@@ -484,63 +488,294 @@ export async function removeAgentGroupMember({
|
|
|
484
488
|
);
|
|
485
489
|
}
|
|
486
490
|
|
|
491
|
+
// ── Workstreams (managed groups) ──
|
|
492
|
+
|
|
493
|
+
export async function createWorkstream({
|
|
494
|
+
actingAgentId, name, description, charter, memberAgentIds,
|
|
495
|
+
}) {
|
|
496
|
+
return apiFetch('/api/agent/workstreams', {
|
|
497
|
+
method: 'POST',
|
|
498
|
+
body: JSON.stringify({
|
|
499
|
+
acting_as_agent_id: actingAgentId,
|
|
500
|
+
name,
|
|
501
|
+
description: description ?? null,
|
|
502
|
+
charter: charter ?? null,
|
|
503
|
+
member_agent_ids: memberAgentIds || [],
|
|
504
|
+
}),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function deleteWorkstream({ actingAgentId, conversationId }) {
|
|
509
|
+
return apiFetch(
|
|
510
|
+
`/api/agent/workstreams/${encodeURIComponent(conversationId)}`,
|
|
511
|
+
{
|
|
512
|
+
method: 'DELETE',
|
|
513
|
+
body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export async function listWorkstreams({ actingAgentId }) {
|
|
519
|
+
const params = new URLSearchParams();
|
|
520
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
521
|
+
const { data } = await apiFetch(`/api/agent/workstreams?${params}`);
|
|
522
|
+
return data || [];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── Agents ──
|
|
526
|
+
|
|
527
|
+
export async function listAgentSlots({ actingAgentId }) {
|
|
528
|
+
const params = new URLSearchParams();
|
|
529
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
530
|
+
const { data } = await apiFetch(`/api/agent/agents?${params}`);
|
|
531
|
+
return data || [];
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function createAgentSlot({
|
|
535
|
+
actingAgentId, name, runtime, description, displayName, model,
|
|
536
|
+
}) {
|
|
537
|
+
return apiFetch('/api/agent/agents', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
body: JSON.stringify({
|
|
540
|
+
acting_as_agent_id: actingAgentId,
|
|
541
|
+
name,
|
|
542
|
+
runtime,
|
|
543
|
+
description: description ?? null,
|
|
544
|
+
display_name: displayName ?? null,
|
|
545
|
+
model: model ?? null,
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function archiveAgentSlot({ actingAgentId, agentId }) {
|
|
551
|
+
return apiFetch(
|
|
552
|
+
`/api/agent/agents/${encodeURIComponent(agentId)}`,
|
|
553
|
+
{
|
|
554
|
+
method: 'DELETE',
|
|
555
|
+
body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
|
|
556
|
+
},
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── Services ──
|
|
561
|
+
|
|
562
|
+
export async function createService({
|
|
563
|
+
actingAgentId, name, description, contractSchema, endpointConfig,
|
|
564
|
+
}) {
|
|
565
|
+
return apiFetch('/api/agent/services', {
|
|
566
|
+
method: 'POST',
|
|
567
|
+
body: JSON.stringify({
|
|
568
|
+
acting_as_agent_id: actingAgentId,
|
|
569
|
+
name,
|
|
570
|
+
description: description ?? null,
|
|
571
|
+
contract_schema: contractSchema ?? null,
|
|
572
|
+
endpoint_config: endpointConfig,
|
|
573
|
+
}),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export async function updateService({ actingAgentId, serviceId, ...patch }) {
|
|
578
|
+
return apiFetch(
|
|
579
|
+
`/api/agent/services/${encodeURIComponent(serviceId)}`,
|
|
580
|
+
{
|
|
581
|
+
method: 'PATCH',
|
|
582
|
+
body: JSON.stringify({ acting_as_agent_id: actingAgentId, ...patch }),
|
|
583
|
+
},
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export async function deleteService({ actingAgentId, serviceId }) {
|
|
588
|
+
return apiFetch(
|
|
589
|
+
`/api/agent/services/${encodeURIComponent(serviceId)}`,
|
|
590
|
+
{
|
|
591
|
+
method: 'DELETE',
|
|
592
|
+
body: JSON.stringify({ acting_as_agent_id: actingAgentId }),
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export async function listServices({ actingAgentId }) {
|
|
598
|
+
const params = new URLSearchParams();
|
|
599
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
600
|
+
const { data } = await apiFetch(`/api/agent/services?${params}`);
|
|
601
|
+
return data || [];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export async function getServiceInfo({ actingAgentId, name }) {
|
|
605
|
+
const params = new URLSearchParams();
|
|
606
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
607
|
+
return apiFetch(
|
|
608
|
+
`/api/agent/services/${encodeURIComponent(name)}/info?${params}`,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export async function callService({ actingAgentId, name, input }) {
|
|
613
|
+
return apiFetch(
|
|
614
|
+
`/api/agent/services/${encodeURIComponent(name)}/call`,
|
|
615
|
+
{
|
|
616
|
+
method: 'POST',
|
|
617
|
+
body: JSON.stringify({ acting_as_agent_id: actingAgentId, input }),
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ── Briefings ──
|
|
623
|
+
|
|
624
|
+
export async function getBriefing({actingAgentId, briefingId}) {
|
|
625
|
+
const params = new URLSearchParams();
|
|
626
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
627
|
+
return apiFetch(`/api/agent/briefings/${encodeURIComponent(briefingId)}?${params}`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export async function publishBriefing({actingAgentId, bodyText, attachmentAssetId, currentConversationId, responseMode}) {
|
|
631
|
+
const body = { acting_as_agent_id: actingAgentId };
|
|
632
|
+
if (bodyText != null) body.body_text = bodyText;
|
|
633
|
+
if (attachmentAssetId != null) body.attachment_asset_id = attachmentAssetId;
|
|
634
|
+
if (currentConversationId != null) body.current_conversation_id = currentConversationId;
|
|
635
|
+
if (responseMode != null) body.response_mode = responseMode;
|
|
636
|
+
return apiFetch('/api/agent/briefings', {
|
|
637
|
+
method: 'POST',
|
|
638
|
+
body: JSON.stringify(body),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ── Credentials (slot creation + daemon sync) ──
|
|
643
|
+
|
|
644
|
+
export async function fetchCredentials() {
|
|
645
|
+
return apiFetch('/api/agent/credentials', { method: 'GET' });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export async function requestCredential({
|
|
649
|
+
actingAgentId, name, description, workstreamId,
|
|
650
|
+
}) {
|
|
651
|
+
return apiFetch('/api/agent/credentials', {
|
|
652
|
+
method: 'POST',
|
|
653
|
+
body: JSON.stringify({
|
|
654
|
+
acting_as_agent_id: actingAgentId,
|
|
655
|
+
name,
|
|
656
|
+
description: description ?? null,
|
|
657
|
+
workstream_id: workstreamId ?? null,
|
|
658
|
+
}),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── Workstream dashboard ──
|
|
663
|
+
|
|
664
|
+
export async function setWorkstreamDashboard({
|
|
665
|
+
actingAgentId, conversationId, dataJson, htmlTemplate,
|
|
666
|
+
}) {
|
|
667
|
+
const body = { acting_as_agent_id: actingAgentId };
|
|
668
|
+
// Distinguish "omit" from "set to null" — only include keys the caller
|
|
669
|
+
// explicitly passed (including null clears the field).
|
|
670
|
+
if (dataJson !== undefined) body.data_json = dataJson;
|
|
671
|
+
if (htmlTemplate !== undefined) body.html_template = htmlTemplate;
|
|
672
|
+
return apiFetch(
|
|
673
|
+
`/api/agent/workstreams/${encodeURIComponent(conversationId)}/dashboard`,
|
|
674
|
+
{ method: 'POST', body: JSON.stringify(body) },
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function getWorkstreamDashboard({ actingAgentId, conversationId }) {
|
|
679
|
+
const params = new URLSearchParams();
|
|
680
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
681
|
+
return apiFetch(
|
|
682
|
+
`/api/agent/workstreams/${encodeURIComponent(conversationId)}/dashboard?${params}`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ── Workstream charter ──
|
|
687
|
+
|
|
688
|
+
export async function getWorkstreamCharter({ actingAgentId, conversationId }) {
|
|
689
|
+
const params = new URLSearchParams();
|
|
690
|
+
params.set('acting_as_agent_id', actingAgentId);
|
|
691
|
+
return apiFetch(
|
|
692
|
+
`/api/agent/workstreams/${encodeURIComponent(conversationId)}/charter?${params}`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export async function setWorkstreamCharter({ actingAgentId, conversationId, charter }) {
|
|
697
|
+
return apiFetch(
|
|
698
|
+
`/api/agent/workstreams/${encodeURIComponent(conversationId)}/charter`,
|
|
699
|
+
{
|
|
700
|
+
method: 'POST',
|
|
701
|
+
body: JSON.stringify({
|
|
702
|
+
acting_as_agent_id: actingAgentId,
|
|
703
|
+
charter: charter ?? null,
|
|
704
|
+
}),
|
|
705
|
+
},
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
487
709
|
// ── Channel event pipe ──
|
|
488
710
|
|
|
489
711
|
export async function postEvent({ agent, agent_id, runtime_host_id, session_id, cwd, runtime_version, event, required = false }) {
|
|
490
712
|
const agentId = agent_id || null;
|
|
491
713
|
const queueKey = agentId || `${agent}:${session_id || ''}`;
|
|
492
|
-
const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
|
|
493
714
|
const eventName = event?.worker_event_name || event?.hook_event_name || event?.event_name || 'unknown';
|
|
494
715
|
const turnId = event?.turn_id || event?.reply_to_message_id || null;
|
|
495
716
|
const seq = event?.event_seq ?? null;
|
|
496
717
|
const deltaChars = typeof event?.delta === 'string' ? event.delta.length : null;
|
|
497
718
|
|
|
719
|
+
if (eventName === 'worker.message.delta') {
|
|
720
|
+
debugLog('events', 'delta.drop', {
|
|
721
|
+
agent,
|
|
722
|
+
agentId,
|
|
723
|
+
sessionId: shortId(session_id),
|
|
724
|
+
turnId: shortId(turnId),
|
|
725
|
+
deltaChars,
|
|
726
|
+
});
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const postOnce = async () => {
|
|
731
|
+
const startedAt = Date.now();
|
|
732
|
+
try {
|
|
733
|
+
const result = await apiFetch('/api/events', {
|
|
734
|
+
method: 'POST',
|
|
735
|
+
body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
|
|
736
|
+
timeout: 5000,
|
|
737
|
+
});
|
|
738
|
+
if (required && result?.matched === false) {
|
|
739
|
+
throw new Error(`event was not matched (${eventName})`);
|
|
740
|
+
}
|
|
741
|
+
debugLog('events', 'post.ok', {
|
|
742
|
+
agent,
|
|
743
|
+
agentId,
|
|
744
|
+
sessionId: shortId(session_id),
|
|
745
|
+
runtimeVersion: runtime_version ?? null,
|
|
746
|
+
turnId: shortId(turnId),
|
|
747
|
+
eventName,
|
|
748
|
+
seq,
|
|
749
|
+
durationMs: Date.now() - startedAt,
|
|
750
|
+
});
|
|
751
|
+
return result;
|
|
752
|
+
} catch (err) {
|
|
753
|
+
debugError('events', 'post.failed', {
|
|
754
|
+
agent,
|
|
755
|
+
agentId,
|
|
756
|
+
sessionId: shortId(session_id),
|
|
757
|
+
runtimeVersion: runtime_version ?? null,
|
|
758
|
+
turnId: shortId(turnId),
|
|
759
|
+
eventName,
|
|
760
|
+
seq,
|
|
761
|
+
durationMs: Date.now() - startedAt,
|
|
762
|
+
error: err?.message || 'unknown error',
|
|
763
|
+
});
|
|
764
|
+
if (required) {
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
if (eventName === 'worker.turn.complete' || eventName === 'worker.turn.error') {
|
|
772
|
+
return postOnce();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const previous = agentEventQueues.get(queueKey) || Promise.resolve(null);
|
|
498
776
|
const queued = previous
|
|
499
777
|
.catch(() => null)
|
|
500
|
-
.then(
|
|
501
|
-
const startedAt = Date.now();
|
|
502
|
-
try {
|
|
503
|
-
const result = await apiFetch('/api/events', {
|
|
504
|
-
method: 'POST',
|
|
505
|
-
body: JSON.stringify({ agent, agent_id: agentId, runtime_host_id, session_id, cwd, runtime_version, event }),
|
|
506
|
-
timeout: 5000,
|
|
507
|
-
});
|
|
508
|
-
if (required && result?.matched === false) {
|
|
509
|
-
throw new Error(`event was not matched (${eventName})`);
|
|
510
|
-
}
|
|
511
|
-
debugLog('events', 'post.ok', {
|
|
512
|
-
agent,
|
|
513
|
-
agentId,
|
|
514
|
-
sessionId: shortId(session_id),
|
|
515
|
-
runtimeVersion: runtime_version ?? null,
|
|
516
|
-
turnId: shortId(turnId),
|
|
517
|
-
eventName,
|
|
518
|
-
seq,
|
|
519
|
-
deltaChars,
|
|
520
|
-
durationMs: Date.now() - startedAt,
|
|
521
|
-
});
|
|
522
|
-
return result;
|
|
523
|
-
} catch (err) {
|
|
524
|
-
debugError('events', 'post.failed', {
|
|
525
|
-
agent,
|
|
526
|
-
agentId,
|
|
527
|
-
sessionId: shortId(session_id),
|
|
528
|
-
runtimeVersion: runtime_version ?? null,
|
|
529
|
-
turnId: shortId(turnId),
|
|
530
|
-
eventName,
|
|
531
|
-
seq,
|
|
532
|
-
deltaChars,
|
|
533
|
-
durationMs: Date.now() - startedAt,
|
|
534
|
-
error: err?.message || 'unknown error',
|
|
535
|
-
});
|
|
536
|
-
if (required) {
|
|
537
|
-
throw err;
|
|
538
|
-
}
|
|
539
|
-
// Best-effort by default. Callers that need a hard failure set
|
|
540
|
-
// `required: true` (used for the final reply path).
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
});
|
|
778
|
+
.then(postOnce);
|
|
544
779
|
|
|
545
780
|
agentEventQueues.set(queueKey, queued);
|
|
546
781
|
queued.finally(() => {
|
|
@@ -6,7 +6,20 @@
|
|
|
6
6
|
* using the connector-specific env name.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { AF_CONFIG_PATH, persistConfig, TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
9
|
+
import { AF_CONFIG_PATH, loadPersistentConfig, persistConfig, TICLAWK_CONNECTOR_API_KEY } from '../../core/config.mjs';
|
|
10
|
+
|
|
11
|
+
export const TICLAWK_CREDENTIAL_NAMES = 'TICLAWK_CREDENTIAL_NAMES';
|
|
12
|
+
|
|
13
|
+
function isRuntimeCredentialName(value) {
|
|
14
|
+
return /^[A-Z][A-Z0-9_]*$/.test(String(value || '').trim());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseCredentialNames(value) {
|
|
18
|
+
return String(value || '')
|
|
19
|
+
.split(',')
|
|
20
|
+
.map((name) => name.trim())
|
|
21
|
+
.filter(isRuntimeCredentialName);
|
|
22
|
+
}
|
|
10
23
|
|
|
11
24
|
export function persistApiCredential(apiKey) {
|
|
12
25
|
if (!apiKey || !apiKey.startsWith('tk_')) return;
|
|
@@ -19,3 +32,30 @@ export function persistApiCredential(apiKey) {
|
|
|
19
32
|
delete process.env.TICLAWK_SETUP_CODE;
|
|
20
33
|
console.log(`[connect] saved ${TICLAWK_CONNECTOR_API_KEY} to ${AF_CONFIG_PATH}`);
|
|
21
34
|
}
|
|
35
|
+
|
|
36
|
+
export function persistRuntimeCredentials(credentials = []) {
|
|
37
|
+
const current = loadPersistentConfig();
|
|
38
|
+
const previousNames = new Set(parseCredentialNames(current[TICLAWK_CREDENTIAL_NAMES]));
|
|
39
|
+
const nextNames = [];
|
|
40
|
+
const updates = {};
|
|
41
|
+
|
|
42
|
+
for (const credential of Array.isArray(credentials) ? credentials : []) {
|
|
43
|
+
const name = String(credential?.name || '').trim();
|
|
44
|
+
const value = typeof credential?.value === 'string' ? credential.value : '';
|
|
45
|
+
if (!isRuntimeCredentialName(name) || !value) continue;
|
|
46
|
+
updates[name] = value;
|
|
47
|
+
nextNames.push(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const nextNameSet = new Set(nextNames);
|
|
51
|
+
for (const name of previousNames) {
|
|
52
|
+
if (!nextNameSet.has(name)) updates[name] = '';
|
|
53
|
+
}
|
|
54
|
+
updates[TICLAWK_CREDENTIAL_NAMES] = nextNames.join(',');
|
|
55
|
+
|
|
56
|
+
persistConfig(updates);
|
|
57
|
+
return {
|
|
58
|
+
saved: nextNames.length,
|
|
59
|
+
removed: [...previousNames].filter((name) => !nextNameSet.has(name)).length,
|
|
60
|
+
};
|
|
61
|
+
}
|