@suwujs/king-ai 0.2.0

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.
Files changed (104) hide show
  1. package/README.md +96 -0
  2. package/dist/src/agent-config-validation.d.ts +9 -0
  3. package/dist/src/agent-config-validation.js +30 -0
  4. package/dist/src/api.d.ts +4 -0
  5. package/dist/src/api.js +48 -0
  6. package/dist/src/attachments.d.ts +45 -0
  7. package/dist/src/attachments.js +322 -0
  8. package/dist/src/cli.d.ts +20 -0
  9. package/dist/src/cli.js +1697 -0
  10. package/dist/src/config.d.ts +3 -0
  11. package/dist/src/config.js +20 -0
  12. package/dist/src/cron.d.ts +11 -0
  13. package/dist/src/cron.js +65 -0
  14. package/dist/src/daemon.d.ts +36 -0
  15. package/dist/src/daemon.js +373 -0
  16. package/dist/src/engine.d.ts +32 -0
  17. package/dist/src/engine.js +1014 -0
  18. package/dist/src/heartbeat.d.ts +18 -0
  19. package/dist/src/heartbeat.js +28 -0
  20. package/dist/src/host-api.d.ts +40 -0
  21. package/dist/src/host-api.js +59 -0
  22. package/dist/src/host-control.d.ts +48 -0
  23. package/dist/src/host-control.js +1279 -0
  24. package/dist/src/host-export.d.ts +50 -0
  25. package/dist/src/host-export.js +187 -0
  26. package/dist/src/host-feedback.d.ts +78 -0
  27. package/dist/src/host-feedback.js +178 -0
  28. package/dist/src/host-home.d.ts +13 -0
  29. package/dist/src/host-home.js +54 -0
  30. package/dist/src/host-ledger.d.ts +261 -0
  31. package/dist/src/host-ledger.js +554 -0
  32. package/dist/src/host-loop-events.d.ts +69 -0
  33. package/dist/src/host-loop-events.js +288 -0
  34. package/dist/src/host-permission.d.ts +36 -0
  35. package/dist/src/host-permission.js +180 -0
  36. package/dist/src/host-policy.d.ts +15 -0
  37. package/dist/src/host-policy.js +36 -0
  38. package/dist/src/host-run-executor.d.ts +13 -0
  39. package/dist/src/host-run-executor.js +221 -0
  40. package/dist/src/host-run-heartbeat.d.ts +40 -0
  41. package/dist/src/host-run-heartbeat.js +103 -0
  42. package/dist/src/host-run-layout.d.ts +17 -0
  43. package/dist/src/host-run-layout.js +387 -0
  44. package/dist/src/host-run-meta.d.ts +41 -0
  45. package/dist/src/host-run-meta.js +115 -0
  46. package/dist/src/host-run-spec.d.ts +149 -0
  47. package/dist/src/host-run-spec.js +465 -0
  48. package/dist/src/host-runs.d.ts +77 -0
  49. package/dist/src/host-runs.js +195 -0
  50. package/dist/src/host-sdk.d.ts +412 -0
  51. package/dist/src/host-sdk.js +628 -0
  52. package/dist/src/host-server.d.ts +26 -0
  53. package/dist/src/host-server.js +921 -0
  54. package/dist/src/host-timeline.d.ts +24 -0
  55. package/dist/src/host-timeline.js +161 -0
  56. package/dist/src/jsonl.d.ts +13 -0
  57. package/dist/src/jsonl.js +47 -0
  58. package/dist/src/lifecycle.d.ts +5 -0
  59. package/dist/src/lifecycle.js +18 -0
  60. package/dist/src/message-routing.d.ts +32 -0
  61. package/dist/src/message-routing.js +119 -0
  62. package/dist/src/paths.d.ts +19 -0
  63. package/dist/src/paths.js +26 -0
  64. package/dist/src/project-profile.d.ts +49 -0
  65. package/dist/src/project-profile.js +356 -0
  66. package/dist/src/remediation.d.ts +14 -0
  67. package/dist/src/remediation.js +114 -0
  68. package/dist/src/remote-devices.d.ts +41 -0
  69. package/dist/src/remote-devices.js +156 -0
  70. package/dist/src/remote-diagnostics.d.ts +39 -0
  71. package/dist/src/remote-diagnostics.js +199 -0
  72. package/dist/src/remote-ssh.d.ts +39 -0
  73. package/dist/src/remote-ssh.js +129 -0
  74. package/dist/src/run-stream.d.ts +57 -0
  75. package/dist/src/run-stream.js +119 -0
  76. package/dist/src/runner.d.ts +131 -0
  77. package/dist/src/runner.js +1161 -0
  78. package/dist/src/runtime-data.d.ts +68 -0
  79. package/dist/src/runtime-data.js +172 -0
  80. package/dist/src/service.d.ts +114 -0
  81. package/dist/src/service.js +631 -0
  82. package/dist/src/shared-skills.d.ts +26 -0
  83. package/dist/src/shared-skills.js +85 -0
  84. package/dist/src/shim.d.ts +1 -0
  85. package/dist/src/shim.js +64 -0
  86. package/dist/src/skill-check.d.ts +17 -0
  87. package/dist/src/skill-check.js +158 -0
  88. package/dist/src/sse.d.ts +9 -0
  89. package/dist/src/sse.js +36 -0
  90. package/dist/src/team-routing.d.ts +55 -0
  91. package/dist/src/team-routing.js +131 -0
  92. package/dist/src/team-workflow.d.ts +78 -0
  93. package/dist/src/team-workflow.js +253 -0
  94. package/dist/src/text.d.ts +7 -0
  95. package/dist/src/text.js +27 -0
  96. package/dist/src/types.d.ts +98 -0
  97. package/dist/src/types.js +1 -0
  98. package/dist/src/usage.d.ts +116 -0
  99. package/dist/src/usage.js +350 -0
  100. package/dist/src/workspace.d.ts +9 -0
  101. package/dist/src/workspace.js +56 -0
  102. package/dist/src/worktree.d.ts +47 -0
  103. package/dist/src/worktree.js +201 -0
  104. package/package.json +63 -0
@@ -0,0 +1,350 @@
1
+ function num(value) {
2
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
3
+ }
4
+ function envNum(value) {
5
+ const parsed = typeof value === "string" ? Number(value) : value;
6
+ return num(parsed);
7
+ }
8
+ export function normalizeEngineUsage(usage) {
9
+ if (!usage || typeof usage !== "object") {
10
+ return { inputTokens: 0, cacheReadInputTokens: 0, outputTokens: 0, totalTokens: 0 };
11
+ }
12
+ const rec = usage;
13
+ const inputTokens = num(rec.input_tokens ?? rec.inputTokens);
14
+ const cacheReadInputTokens = num(rec.cache_read_input_tokens ?? rec.cacheReadInputTokens ?? rec.cachedInputTokens);
15
+ const outputTokens = num(rec.output_tokens ?? rec.outputTokens);
16
+ const totalTokens = num(rec.total_tokens ?? rec.totalTokens) || inputTokens + cacheReadInputTokens + outputTokens;
17
+ return { inputTokens, cacheReadInputTokens, outputTokens, totalTokens };
18
+ }
19
+ export function emptyAgentRunStats() {
20
+ return {
21
+ turns: 0,
22
+ completed: 0,
23
+ failed: 0,
24
+ inputTokens: 0,
25
+ cacheReadInputTokens: 0,
26
+ outputTokens: 0,
27
+ totalTokens: 0
28
+ };
29
+ }
30
+ export function recordAgentRunStats(current, args) {
31
+ const usage = normalizeEngineUsage(args.usage);
32
+ return {
33
+ turns: current.turns + 1,
34
+ completed: current.completed + (args.status === "completed" ? 1 : 0),
35
+ failed: current.failed + (args.status === "failed" ? 1 : 0),
36
+ inputTokens: current.inputTokens + usage.inputTokens,
37
+ cacheReadInputTokens: current.cacheReadInputTokens + usage.cacheReadInputTokens,
38
+ outputTokens: current.outputTokens + usage.outputTokens,
39
+ totalTokens: current.totalTokens + usage.totalTokens,
40
+ lastRunAt: args.at ?? new Date().toISOString(),
41
+ lastDurationMs: Math.max(0, Math.floor(args.durationMs)),
42
+ lastStatus: args.status,
43
+ lastModel: args.model ?? null
44
+ };
45
+ }
46
+ export function formatAgentRunStats(stats) {
47
+ if (!stats || stats.turns === 0)
48
+ return "";
49
+ const tokens = stats.totalTokens
50
+ ? `${stats.totalTokens} tokens (in=${stats.inputTokens}, cache=${stats.cacheReadInputTokens}, out=${stats.outputTokens})`
51
+ : "tokens unavailable";
52
+ const last = stats.lastRunAt
53
+ ? ` last=${stats.lastStatus ?? "unknown"} ${stats.lastDurationMs ?? 0}ms @ ${stats.lastRunAt}${stats.lastModel ? ` model=${stats.lastModel}` : ""}`
54
+ : "";
55
+ return `runs=${stats.turns} completed=${stats.completed} failed=${stats.failed} ${tokens}${last}`;
56
+ }
57
+ export function tokenBudgetFromEnv(env = process.env) {
58
+ const budget = envNum(env.KING_AI_TOKEN_BUDGET);
59
+ return budget > 0 ? budget : null;
60
+ }
61
+ export function usagePricingFromEnv(env = process.env) {
62
+ const raw = env.KING_AI_USAGE_PRICING;
63
+ if (!raw)
64
+ return [];
65
+ try {
66
+ return normalizeUsagePricing(JSON.parse(raw));
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ }
72
+ export function normalizeUsagePricing(value) {
73
+ if (Array.isArray(value))
74
+ return value.flatMap((entry) => normalizeUsagePricingRule(entry));
75
+ if (!value || typeof value !== "object")
76
+ return [];
77
+ return Object.entries(value).flatMap(([key, entry]) => normalizeUsagePricingRule({
78
+ ...(entry && typeof entry === "object" ? entry : {}),
79
+ key
80
+ }));
81
+ }
82
+ export function estimateUsageCost(totals, pricing) {
83
+ if (!pricing)
84
+ return undefined;
85
+ const inputRate = pricing.inputPerMillionTokens ?? 0;
86
+ const cacheRate = pricing.cacheReadInputPerMillionTokens ?? inputRate;
87
+ const outputRate = pricing.outputPerMillionTokens ?? 0;
88
+ const inputCost = costForTokens(totals.inputTokens, inputRate);
89
+ const cacheReadInputCost = costForTokens(totals.cacheReadInputTokens, cacheRate);
90
+ const outputCost = costForTokens(totals.outputTokens, outputRate);
91
+ const pricedTokens = (inputRate > 0 ? totals.inputTokens : 0) +
92
+ (cacheRate > 0 ? totals.cacheReadInputTokens : 0) +
93
+ (outputRate > 0 ? totals.outputTokens : 0);
94
+ return {
95
+ amount: roundCost(inputCost + cacheReadInputCost + outputCost),
96
+ currency: pricing.currency,
97
+ inputCost,
98
+ cacheReadInputCost,
99
+ outputCost,
100
+ pricedTokens,
101
+ unpricedTokens: Math.max(0, totals.totalTokens - pricedTokens),
102
+ pricingKeys: [pricing.key]
103
+ };
104
+ }
105
+ export function checkTokenBudget(stats, budget) {
106
+ if (!stats || !budget)
107
+ return undefined;
108
+ const used = stats.totalTokens;
109
+ const remaining = budget - used;
110
+ const exceeded = remaining < 0;
111
+ const warning = !exceeded && remaining < budget * 0.2;
112
+ return {
113
+ budget,
114
+ used,
115
+ remaining,
116
+ warning,
117
+ exceeded,
118
+ state: exceeded ? "exceeded" : warning ? "warning" : "ok"
119
+ };
120
+ }
121
+ export function formatTokenBudgetCheck(check) {
122
+ if (!check)
123
+ return "";
124
+ return `budget=${check.budget} used=${check.used} remaining=${check.remaining} state=${check.state}`;
125
+ }
126
+ export function summarizeAgentUsage(agents = [], budget = null, pricingRules = []) {
127
+ const summary = {
128
+ agents: [],
129
+ byEngine: [],
130
+ byModel: [],
131
+ turns: 0,
132
+ completed: 0,
133
+ failed: 0,
134
+ inputTokens: 0,
135
+ cacheReadInputTokens: 0,
136
+ outputTokens: 0,
137
+ totalTokens: 0
138
+ };
139
+ const byEngine = new Map();
140
+ const byModel = new Map();
141
+ const pricing = pricingRules.map((rule) => ({ ...rule, key: rule.key.toLowerCase() }));
142
+ for (const agent of agents) {
143
+ const stats = agent.runStats ?? emptyAgentRunStats();
144
+ const model = agent.model ?? stats.lastModel ?? null;
145
+ const cost = estimateUsageCost(stats, selectUsagePricingRule(pricing, agent.engine, model));
146
+ const agentSummary = {
147
+ key: agent.id,
148
+ id: agent.id,
149
+ name: agent.name,
150
+ engine: agent.engine,
151
+ model,
152
+ turns: stats.turns,
153
+ completed: stats.completed,
154
+ failed: stats.failed,
155
+ agents: 1,
156
+ inputTokens: stats.inputTokens,
157
+ cacheReadInputTokens: stats.cacheReadInputTokens,
158
+ outputTokens: stats.outputTokens,
159
+ totalTokens: stats.totalTokens,
160
+ lastRunAt: stats.lastRunAt,
161
+ lastStatus: stats.lastStatus,
162
+ tokenBudget: agent.tokenBudget,
163
+ cost
164
+ };
165
+ summary.agents.push(agentSummary);
166
+ addUsageTotals(summary, stats);
167
+ addUsageCost(summary, cost);
168
+ summary.turns += stats.turns;
169
+ summary.completed += stats.completed;
170
+ summary.failed += stats.failed;
171
+ addGroup(byEngine, agent.engine || "unknown", stats, cost);
172
+ addGroup(byModel, agentSummary.model || "default", stats, cost);
173
+ }
174
+ summary.agents.sort((left, right) => right.totalTokens - left.totalTokens || left.id.localeCompare(right.id));
175
+ summary.byEngine = [...byEngine.values()].sort((left, right) => right.totalTokens - left.totalTokens || left.key.localeCompare(right.key));
176
+ summary.byModel = [...byModel.values()].sort((left, right) => right.totalTokens - left.totalTokens || left.key.localeCompare(right.key));
177
+ summary.budget = checkTokenBudget(summary, budget);
178
+ return summary;
179
+ }
180
+ export function formatUsageSummary(summary) {
181
+ const lines = [
182
+ "usage summary:",
183
+ ` runs=${summary.turns} completed=${summary.completed} failed=${summary.failed}`,
184
+ ` tokens=${summary.totalTokens} input=${summary.inputTokens} cache=${summary.cacheReadInputTokens} output=${summary.outputTokens}`
185
+ ];
186
+ const budget = formatTokenBudgetCheck(summary.budget);
187
+ if (budget)
188
+ lines.push(` token budget: ${budget}`);
189
+ if (summary.cost)
190
+ lines.push(` estimated cost: ${formatUsageCost(summary.cost)}`);
191
+ if (summary.byEngine.length) {
192
+ lines.push("by engine:");
193
+ for (const group of summary.byEngine)
194
+ lines.push(` - ${formatUsageGroup(group)}`);
195
+ }
196
+ if (summary.byModel.length) {
197
+ lines.push("by model:");
198
+ for (const group of summary.byModel)
199
+ lines.push(` - ${formatUsageGroup(group)}`);
200
+ }
201
+ if (summary.agents.length) {
202
+ lines.push("by agent:");
203
+ for (const agent of summary.agents) {
204
+ const label = agent.name ? `${agent.id} (${agent.name})` : agent.id;
205
+ const budgetText = formatTokenBudgetCheck(agent.tokenBudget);
206
+ const costText = agent.cost ? ` cost=${formatUsageCost(agent.cost)}` : "";
207
+ lines.push(` - ${label}: engine=${agent.engine} model=${agent.model || "default"} ${formatUsageGroup(agent)}${agent.lastRunAt ? ` last=${agent.lastStatus ?? "unknown"}@${agent.lastRunAt}` : ""}${budgetText ? ` ${budgetText}` : ""}${costText}`);
208
+ }
209
+ }
210
+ else {
211
+ lines.push("by agent: none");
212
+ }
213
+ return lines.join("\n");
214
+ }
215
+ export function listUsageExpenses(summary) {
216
+ return summary.agents
217
+ .filter((agent) => agent.cost || agent.totalTokens > 0 || agent.turns > 0)
218
+ .map((agent) => {
219
+ const cost = agent.cost;
220
+ return {
221
+ agentId: agent.id,
222
+ agentName: agent.name,
223
+ engine: agent.engine,
224
+ model: agent.model || "default",
225
+ turns: agent.turns,
226
+ completed: agent.completed,
227
+ failed: agent.failed,
228
+ inputTokens: agent.inputTokens,
229
+ cacheReadInputTokens: agent.cacheReadInputTokens,
230
+ outputTokens: agent.outputTokens,
231
+ totalTokens: agent.totalTokens,
232
+ currency: cost?.currency ?? "",
233
+ amount: cost?.amount ?? 0,
234
+ inputCost: cost?.inputCost ?? 0,
235
+ cacheReadInputCost: cost?.cacheReadInputCost ?? 0,
236
+ outputCost: cost?.outputCost ?? 0,
237
+ pricedTokens: cost?.pricedTokens ?? 0,
238
+ unpricedTokens: cost?.unpricedTokens ?? agent.totalTokens,
239
+ pricingKeys: cost?.pricingKeys ?? [],
240
+ lastRunAt: agent.lastRunAt,
241
+ lastStatus: agent.lastStatus
242
+ };
243
+ })
244
+ .sort((left, right) => right.amount - left.amount || right.totalTokens - left.totalTokens || left.agentId.localeCompare(right.agentId));
245
+ }
246
+ export function formatUsageExpenses(rows) {
247
+ if (rows.length === 0)
248
+ return "usage expenses: none";
249
+ const lines = ["usage expenses:"];
250
+ for (const row of rows) {
251
+ const label = row.agentName ? `${row.agentId} (${row.agentName})` : row.agentId;
252
+ const cost = row.currency ? `${row.currency} ${row.amount.toFixed(6)}` : "unpriced";
253
+ const unpriced = row.unpricedTokens ? ` unpricedTokens=${row.unpricedTokens}` : "";
254
+ const keys = row.pricingKeys.length ? ` pricing=${row.pricingKeys.join(",")}` : "";
255
+ const last = row.lastRunAt ? ` last=${row.lastStatus ?? "unknown"}@${row.lastRunAt}` : "";
256
+ lines.push(` - ${label}: ${cost} engine=${row.engine} model=${row.model} runs=${row.turns} completed=${row.completed} failed=${row.failed} tokens=${row.totalTokens} input=${row.inputTokens} cache=${row.cacheReadInputTokens} output=${row.outputTokens} inputCost=${row.inputCost.toFixed(6)} cacheCost=${row.cacheReadInputCost.toFixed(6)} outputCost=${row.outputCost.toFixed(6)}${unpriced}${keys}${last}`);
257
+ }
258
+ return lines.join("\n");
259
+ }
260
+ function addGroup(groups, key, stats, cost) {
261
+ const group = groups.get(key) ?? {
262
+ key,
263
+ agents: 0,
264
+ turns: 0,
265
+ completed: 0,
266
+ failed: 0,
267
+ inputTokens: 0,
268
+ cacheReadInputTokens: 0,
269
+ outputTokens: 0,
270
+ totalTokens: 0
271
+ };
272
+ group.agents += 1;
273
+ group.turns += stats.turns;
274
+ group.completed += stats.completed;
275
+ group.failed += stats.failed;
276
+ addUsageTotals(group, stats);
277
+ addUsageCost(group, cost);
278
+ groups.set(key, group);
279
+ }
280
+ function addUsageTotals(target, source) {
281
+ target.inputTokens += source.inputTokens;
282
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
283
+ target.outputTokens += source.outputTokens;
284
+ target.totalTokens += source.totalTokens;
285
+ }
286
+ function addUsageCost(target, source) {
287
+ if (!source)
288
+ return;
289
+ if (!target.cost) {
290
+ target.cost = { ...source, pricingKeys: [...source.pricingKeys] };
291
+ return;
292
+ }
293
+ if (target.cost.currency !== source.currency) {
294
+ target.cost.unpricedTokens += source.pricedTokens + source.unpricedTokens;
295
+ return;
296
+ }
297
+ target.cost.amount = roundCost(target.cost.amount + source.amount);
298
+ target.cost.inputCost = roundCost(target.cost.inputCost + source.inputCost);
299
+ target.cost.cacheReadInputCost = roundCost(target.cost.cacheReadInputCost + source.cacheReadInputCost);
300
+ target.cost.outputCost = roundCost(target.cost.outputCost + source.outputCost);
301
+ target.cost.pricedTokens += source.pricedTokens;
302
+ target.cost.unpricedTokens += source.unpricedTokens;
303
+ target.cost.pricingKeys = [...new Set([...target.cost.pricingKeys, ...source.pricingKeys])];
304
+ }
305
+ function normalizeUsagePricingRule(value) {
306
+ if (!value || typeof value !== "object")
307
+ return [];
308
+ const entry = value;
309
+ const key = typeof entry.key === "string" ? entry.key.trim().toLowerCase() : "";
310
+ if (!key)
311
+ return [];
312
+ const rule = {
313
+ key,
314
+ currency: typeof entry.currency === "string" && entry.currency.trim() ? entry.currency.trim().toUpperCase() : "USD",
315
+ inputPerMillionTokens: optionalRate(entry.inputPerMillionTokens ?? entry.inputPerMillion ?? entry.input_tokens_per_million),
316
+ cacheReadInputPerMillionTokens: optionalRate(entry.cacheReadInputPerMillionTokens ?? entry.cacheReadInputPerMillion ?? entry.cachedInputPerMillion ?? entry.cache_read_input_tokens_per_million),
317
+ outputPerMillionTokens: optionalRate(entry.outputPerMillionTokens ?? entry.outputPerMillion ?? entry.output_tokens_per_million),
318
+ source: typeof entry.source === "string" ? entry.source : undefined
319
+ };
320
+ return rule.inputPerMillionTokens || rule.cacheReadInputPerMillionTokens || rule.outputPerMillionTokens ? [rule] : [];
321
+ }
322
+ function selectUsagePricingRule(pricing, engine, model) {
323
+ const engineKey = engine.trim().toLowerCase();
324
+ const modelKey = model?.trim().toLowerCase();
325
+ const candidates = [
326
+ modelKey ? `${engineKey}:${modelKey}` : "",
327
+ modelKey ?? "",
328
+ `${engineKey}:*`,
329
+ engineKey,
330
+ "*"
331
+ ].filter(Boolean);
332
+ return candidates.map((key) => pricing.find((rule) => rule.key === key)).find((rule) => Boolean(rule));
333
+ }
334
+ function optionalRate(value) {
335
+ const parsed = typeof value === "string" ? Number(value) : value;
336
+ return typeof parsed === "number" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
337
+ }
338
+ function costForTokens(tokens, perMillionTokens) {
339
+ return roundCost(tokens / 1_000_000 * perMillionTokens);
340
+ }
341
+ function roundCost(value) {
342
+ return Math.round(value * 1_000_000) / 1_000_000;
343
+ }
344
+ function formatUsageCost(cost) {
345
+ const unpriced = cost.unpricedTokens ? ` unpricedTokens=${cost.unpricedTokens}` : "";
346
+ return `${cost.currency} ${cost.amount.toFixed(6)}${unpriced}`;
347
+ }
348
+ function formatUsageGroup(group) {
349
+ return `${group.key}: runs=${group.turns} completed=${group.completed} failed=${group.failed} tokens=${group.totalTokens} input=${group.inputTokens} cache=${group.cacheReadInputTokens} output=${group.outputTokens}`;
350
+ }
@@ -0,0 +1,9 @@
1
+ export interface LocalCapabilities {
2
+ workspaces: string[];
3
+ agentWorkspaceRoot?: string;
4
+ }
5
+ export declare function resolveWorkspaceAllowlist(env?: NodeJS.ProcessEnv): string[];
6
+ export declare function detectLocalCapabilities(env?: NodeJS.ProcessEnv): LocalCapabilities;
7
+ export declare function resolveAgentWorkspaceBase(env?: NodeJS.ProcessEnv): string | undefined;
8
+ export declare function agentWorkspaceRoot(agentId: string, agentHome: string, env?: NodeJS.ProcessEnv): string;
9
+ export declare function formatWorkspacePolicy(workspaces: string[], agentRoot?: string): string;
@@ -0,0 +1,56 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { delimiter, join, resolve } from "node:path";
4
+ function splitList(value) {
5
+ if (!value)
6
+ return [];
7
+ return value
8
+ .split(delimiter)
9
+ .flatMap((part) => part.split(","))
10
+ .map((part) => part.trim())
11
+ .filter(Boolean);
12
+ }
13
+ export function resolveWorkspaceAllowlist(env = process.env) {
14
+ const explicit = splitList(env.KING_AI_WORKSPACES);
15
+ const candidates = explicit.length > 0 ? explicit : [resolve(homedir(), "workspace")];
16
+ const seen = new Set();
17
+ const result = [];
18
+ for (const candidate of candidates) {
19
+ const resolved = resolve(candidate.replace(/^~(?=$|\/|\\)/, homedir()));
20
+ if (seen.has(resolved))
21
+ continue;
22
+ seen.add(resolved);
23
+ if (explicit.length > 0 || existsSync(resolved))
24
+ result.push(resolved);
25
+ }
26
+ return result;
27
+ }
28
+ export function detectLocalCapabilities(env = process.env) {
29
+ return {
30
+ workspaces: resolveWorkspaceAllowlist(env),
31
+ agentWorkspaceRoot: resolveAgentWorkspaceBase(env)
32
+ };
33
+ }
34
+ export function resolveAgentWorkspaceBase(env = process.env) {
35
+ const raw = env.KING_AI_AGENT_WORKSPACE_ROOT;
36
+ return raw ? resolve(raw.replace(/^~(?=$|\/|\\)/, homedir())) : undefined;
37
+ }
38
+ export function agentWorkspaceRoot(agentId, agentHome, env = process.env) {
39
+ const base = resolveAgentWorkspaceBase(env);
40
+ return base ? join(base, agentId) : join(agentHome, "workspace");
41
+ }
42
+ export function formatWorkspacePolicy(workspaces, agentRoot) {
43
+ const rootLine = agentRoot ? `Agent workspace root: ${agentRoot}. Use this as your default project workspace for clones, builds, downloads, and scratch files.` : "";
44
+ if (workspaces.length === 0) {
45
+ return [
46
+ rootLine,
47
+ "Workspace access: no external workspace directories are explicitly allowed. Stay in your agent workspace root unless the operator configures KING_AI_WORKSPACES."
48
+ ].filter(Boolean).join("\n");
49
+ }
50
+ return [
51
+ rootLine,
52
+ "Workspace access: the operator has allowed these external workspace directories for this agent:",
53
+ ...workspaces.map((path) => `- ${path}`),
54
+ "You may read or work in those directories only when the runtime task asks for it. For unrelated scratch work, use your agent workspace root."
55
+ ].filter(Boolean).join("\n");
56
+ }
@@ -0,0 +1,47 @@
1
+ export interface WorktreePlan {
2
+ repoRoot: string;
3
+ repoName: string;
4
+ repoUrl?: string;
5
+ branch: string;
6
+ worktreePath: string;
7
+ command: string[];
8
+ }
9
+ export type WorktreePreparationStatus = "planned" | "created" | "exists" | "failed";
10
+ export type WorktreeCleanupStatus = "missing" | "present" | "removed" | "failed";
11
+ export interface WorktreePreparationResult {
12
+ plan: WorktreePlan;
13
+ status: WorktreePreparationStatus;
14
+ commandText: string;
15
+ detail?: string;
16
+ }
17
+ export interface WorktreeCleanupResult {
18
+ plan: WorktreePlan;
19
+ status: WorktreeCleanupStatus;
20
+ commandText: string;
21
+ detail?: string;
22
+ }
23
+ export type WorktreeExecutor = (file: string, args: string[]) => Promise<unknown>;
24
+ export declare function safeBranchSegment(value: string): string;
25
+ export declare function isGitRepo(path: string): boolean;
26
+ export declare function isGitHubRepoUrl(repoUrl: string): boolean;
27
+ export declare function gitOriginUrl(repoRoot: string): string | undefined;
28
+ export declare function githubRepoUrl(repoRoot: string): string | undefined;
29
+ export declare function planAgentWorktrees(args: {
30
+ agentId: string;
31
+ workspaces: string[];
32
+ baseRoot?: string;
33
+ }): WorktreePlan[];
34
+ export declare function formatWorktreePlanForPrompt(plans: WorktreePlan[]): string;
35
+ export declare function shellQuote(value: string): string;
36
+ export declare function commandText(command: string[]): string;
37
+ export declare function prepareWorktreePlans(plans: WorktreePlan[], options?: {
38
+ execute?: boolean;
39
+ executor?: WorktreeExecutor;
40
+ }): Promise<WorktreePreparationResult[]>;
41
+ export declare function formatWorktreePreparationResults(results: WorktreePreparationResult[], execute?: boolean): string;
42
+ export declare function cleanupCommand(plan: WorktreePlan): string[];
43
+ export declare function cleanupWorktreePlans(plans: WorktreePlan[], options?: {
44
+ execute?: boolean;
45
+ executor?: WorktreeExecutor;
46
+ }): Promise<WorktreeCleanupResult[]>;
47
+ export declare function formatWorktreeCleanupResults(results: WorktreeCleanupResult[], execute?: boolean): string;
@@ -0,0 +1,201 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { execFile } from "node:child_process";
6
+ const execFileP = promisify(execFile);
7
+ export function safeBranchSegment(value) {
8
+ return value
9
+ .trim()
10
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
11
+ .replace(/^-+|-+$/g, "")
12
+ .slice(0, 80) || "agent";
13
+ }
14
+ export function isGitRepo(path) {
15
+ return existsSync(join(path, ".git"));
16
+ }
17
+ const SSH_GITHUB_REPO_RE = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i;
18
+ function hasValidRepoPathname(pathname) {
19
+ const segments = pathname.split("/").filter(Boolean);
20
+ if (segments.length !== 2)
21
+ return false;
22
+ return segments.every((segment, index) => {
23
+ const normalized = index === 1 ? segment.replace(/\.git$/i, "") : segment;
24
+ return normalized.length > 0 && !normalized.startsWith(".");
25
+ });
26
+ }
27
+ export function isGitHubRepoUrl(repoUrl) {
28
+ const trimmed = repoUrl.trim();
29
+ if (!trimmed)
30
+ return false;
31
+ if (SSH_GITHUB_REPO_RE.test(trimmed))
32
+ return true;
33
+ let parsed;
34
+ try {
35
+ parsed = new URL(trimmed);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ if (!["https:", "http:", "ssh:"].includes(parsed.protocol))
41
+ return false;
42
+ if (parsed.hostname.toLowerCase() !== "github.com")
43
+ return false;
44
+ if (parsed.username && parsed.username !== "git")
45
+ return false;
46
+ if (parsed.password || parsed.search || parsed.hash)
47
+ return false;
48
+ return hasValidRepoPathname(parsed.pathname);
49
+ }
50
+ export function gitOriginUrl(repoRoot) {
51
+ const configPath = join(repoRoot, ".git", "config");
52
+ if (!existsSync(configPath))
53
+ return undefined;
54
+ const config = readFileSync(configPath, "utf8");
55
+ const lines = config.split(/\r?\n/);
56
+ let inOrigin = false;
57
+ for (const line of lines) {
58
+ const section = line.match(/^\s*\[remote\s+"([^"]+)"\]\s*$/);
59
+ if (section) {
60
+ inOrigin = section[1] === "origin";
61
+ continue;
62
+ }
63
+ if (!inOrigin)
64
+ continue;
65
+ const url = line.match(/^\s*url\s*=\s*(.+?)\s*$/);
66
+ if (url?.[1])
67
+ return url[1];
68
+ }
69
+ return undefined;
70
+ }
71
+ export function githubRepoUrl(repoRoot) {
72
+ const origin = gitOriginUrl(repoRoot);
73
+ return origin && isGitHubRepoUrl(origin) ? origin : undefined;
74
+ }
75
+ export function planAgentWorktrees(args) {
76
+ const agentSegment = safeBranchSegment(args.agentId);
77
+ return args.workspaces
78
+ .filter((workspace) => isGitRepo(workspace))
79
+ .map((repoRoot) => {
80
+ const repoName = basename(repoRoot) || "repo";
81
+ const branch = `agent/${agentSegment}`;
82
+ const worktreePath = args.baseRoot ? join(args.baseRoot, repoName) : join(repoRoot, ".worktrees", agentSegment);
83
+ return {
84
+ repoRoot,
85
+ repoName,
86
+ repoUrl: githubRepoUrl(repoRoot),
87
+ branch,
88
+ worktreePath,
89
+ command: ["git", "-C", repoRoot, "worktree", "add", "-B", branch, worktreePath]
90
+ };
91
+ });
92
+ }
93
+ export function formatWorktreePlanForPrompt(plans) {
94
+ if (plans.length === 0)
95
+ return "";
96
+ return [
97
+ "Planned git worktree isolation (not auto-created by the daemon):",
98
+ ...plans.map((plan) => `- ${plan.repoName}: ${plan.worktreePath} on ${plan.branch}${plan.repoUrl ? ` from ${plan.repoUrl}` : ""}`)
99
+ ].join("\n");
100
+ }
101
+ export function shellQuote(value) {
102
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
103
+ }
104
+ export function commandText(command) {
105
+ return command.map(shellQuote).join(" ");
106
+ }
107
+ export async function prepareWorktreePlans(plans, options = {}) {
108
+ const executor = options.executor ?? ((file, args) => execFileP(file, args));
109
+ const results = [];
110
+ for (const plan of plans) {
111
+ const command = commandText(plan.command);
112
+ if (existsSync(plan.worktreePath)) {
113
+ results.push({ plan, status: "exists", commandText: command, detail: "worktree path already exists" });
114
+ continue;
115
+ }
116
+ if (!options.execute) {
117
+ results.push({ plan, status: "planned", commandText: command });
118
+ continue;
119
+ }
120
+ try {
121
+ await mkdir(dirname(plan.worktreePath), { recursive: true });
122
+ const [file, ...args] = plan.command;
123
+ if (!file)
124
+ throw new Error("empty worktree command");
125
+ await executor(file, args);
126
+ results.push({ plan, status: "created", commandText: command });
127
+ }
128
+ catch (err) {
129
+ results.push({ plan, status: "failed", commandText: command, detail: err instanceof Error ? err.message : String(err) });
130
+ }
131
+ }
132
+ return results;
133
+ }
134
+ export function formatWorktreePreparationResults(results, execute = false) {
135
+ if (results.length === 0)
136
+ return "no worktree plans found";
137
+ const lines = [`worktree preparation${execute ? "" : " (dry run)"}`];
138
+ for (const result of results) {
139
+ lines.push(`- ${result.plan.repoName}: ${result.status}`);
140
+ lines.push(` repo: ${result.plan.repoRoot}`);
141
+ if (result.plan.repoUrl)
142
+ lines.push(` origin: ${result.plan.repoUrl}`);
143
+ lines.push(` path: ${result.plan.worktreePath}`);
144
+ lines.push(` branch: ${result.plan.branch}`);
145
+ lines.push(` command: ${result.commandText}`);
146
+ if (result.detail)
147
+ lines.push(` detail: ${result.detail}`);
148
+ }
149
+ if (!execute)
150
+ lines.push("rerun with --yes to create these worktrees");
151
+ return lines.join("\n");
152
+ }
153
+ export function cleanupCommand(plan) {
154
+ return ["git", "-C", plan.repoRoot, "worktree", "remove", "--force", plan.worktreePath];
155
+ }
156
+ export async function cleanupWorktreePlans(plans, options = {}) {
157
+ const executor = options.executor ?? ((file, args) => execFileP(file, args));
158
+ const results = [];
159
+ for (const plan of plans) {
160
+ const removeCommand = cleanupCommand(plan);
161
+ const command = commandText(removeCommand);
162
+ if (!existsSync(plan.worktreePath)) {
163
+ results.push({ plan, status: "missing", commandText: command, detail: "worktree path does not exist" });
164
+ continue;
165
+ }
166
+ if (!options.execute) {
167
+ results.push({ plan, status: "present", commandText: command });
168
+ continue;
169
+ }
170
+ try {
171
+ const [file, ...args] = removeCommand;
172
+ if (!file)
173
+ throw new Error("empty cleanup command");
174
+ await executor(file, args);
175
+ results.push({ plan, status: "removed", commandText: command });
176
+ }
177
+ catch (err) {
178
+ results.push({ plan, status: "failed", commandText: command, detail: err instanceof Error ? err.message : String(err) });
179
+ }
180
+ }
181
+ return results;
182
+ }
183
+ export function formatWorktreeCleanupResults(results, execute = false) {
184
+ if (results.length === 0)
185
+ return "no worktree plans found";
186
+ const lines = [`worktree cleanup${execute ? "" : " (dry run)"}`];
187
+ for (const result of results) {
188
+ lines.push(`- ${result.plan.repoName}: ${result.status}`);
189
+ lines.push(` repo: ${result.plan.repoRoot}`);
190
+ if (result.plan.repoUrl)
191
+ lines.push(` origin: ${result.plan.repoUrl}`);
192
+ lines.push(` path: ${result.plan.worktreePath}`);
193
+ lines.push(` branch: ${result.plan.branch}`);
194
+ lines.push(` command: ${result.commandText}`);
195
+ if (result.detail)
196
+ lines.push(` detail: ${result.detail}`);
197
+ }
198
+ if (!execute)
199
+ lines.push("rerun with --yes to remove present worktrees");
200
+ return lines.join("\n");
201
+ }