threadwire 0.1.8 → 0.1.9

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.
@@ -0,0 +1,165 @@
1
+ // @ts-check
2
+
3
+ const EXECUTABLE = "/usr/local/bin/kimi"
4
+ const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u
5
+ const SESSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u
6
+ const TOOL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u
7
+ const TOOL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.:-]{0,127}$/u
8
+
9
+ /** @typedef {import("../types.js").WorkerEvent} WorkerEvent */
10
+ /** @typedef {{started: boolean, sessionEmitted: boolean, tools: Map<string, string>}} KimiRecordState */
11
+ /** @typedef {{trusted: boolean, sessionId: string | undefined, events: WorkerEvent[]}} KimiRecognition */
12
+
13
+ /** @param {string[]} providerArguments */
14
+ export function validateKimiProviderArguments(providerArguments) {
15
+ if (providerArguments.length === 0) return {modelAlias: "default", arguments: []}
16
+ if (providerArguments.length === 2 && (providerArguments[0] === "--model" || providerArguments[0] === "-m")
17
+ && ALIAS_PATTERN.test(providerArguments[1] ?? "")) {
18
+ return {modelAlias: /** @type {string} */ (providerArguments[1]), arguments: ["--model", /** @type {string} */ (providerArguments[1])]}
19
+ }
20
+ throw new Error("Threadwire owns Kimi prompt, output, session, model configuration, permissions, and extensions")
21
+ }
22
+
23
+ /** @param {string[]} providerArguments @param {string} prompt @param {string | undefined} [resumeSession] */
24
+ export function buildKimiCommand(providerArguments, prompt, resumeSession) {
25
+ const selection = validateKimiProviderArguments(providerArguments)
26
+ if (resumeSession !== undefined && !SESSION_PATTERN.test(resumeSession)) throw new Error("Kimi session ID is invalid")
27
+ return {
28
+ executable: EXECUTABLE,
29
+ arguments: [
30
+ "--model", selection.modelAlias,
31
+ ...(resumeSession === undefined ? [] : ["--session", resumeSession]),
32
+ "--prompt", prompt,
33
+ "--output-format", "stream-json"
34
+ ]
35
+ }
36
+ }
37
+
38
+ /** @returns {(record: unknown) => string | undefined} */
39
+ export function createKimiSessionId() {
40
+ let emitted = false
41
+ return (record) => {
42
+ if (emitted) return undefined
43
+ const recognized = recognizeKimiRecord(record, undefined)
44
+ if (recognized.sessionId === undefined) return undefined
45
+ emitted = true
46
+ return recognized.sessionId
47
+ }
48
+ }
49
+
50
+ /** @returns {(record: unknown) => import("../types.js").WorkerEvent[]} */
51
+ export function createKimiParser() {
52
+ const state = createKimiRecordState()
53
+ return (record) => recognizeKimiRecord(record, state).events
54
+ }
55
+
56
+ /** @param {unknown} record @returns {import("../types.js").WorkerEvent[]} */
57
+ export function parseKimiEvent(record) {
58
+ return recognizeKimiRecord(record, createKimiRecordState()).events
59
+ }
60
+
61
+ /** @returns {KimiRecordState} */
62
+ export function createKimiRecordState() {
63
+ return {started: false, sessionEmitted: false, tools: new Map()}
64
+ }
65
+
66
+ /** @param {unknown} record */
67
+ export function kimiSessionEnvelopeId(record) {
68
+ return isRecord(record) && exactKeys(record, ["type", "sessionId"])
69
+ && record.type === "session" && typeof record.sessionId === "string" && SESSION_PATTERN.test(record.sessionId)
70
+ ? record.sessionId
71
+ : undefined
72
+ }
73
+
74
+ /**
75
+ * Convert one pinned Kimi stream-json record into the only envelopes the
76
+ * isolated worker may expose. Raw arguments, results, metadata, and stderr are
77
+ * never represented in the return value.
78
+ * @param {unknown} record
79
+ * @param {ReturnType<typeof createKimiRecordState>} state
80
+ */
81
+ export function sanitizeKimiRecord(record, state) {
82
+ const recognized = recognizeKimiRecord(record, state)
83
+ /** @type {({type: "worker-event", event: WorkerEvent} | {type: "session", sessionId: string})[]} */
84
+ const output = recognized.events.map((event) => ({type: /** @type {const} */ ("worker-event"), event}))
85
+ if (recognized.sessionId !== undefined && !state.sessionEmitted) {
86
+ state.sessionEmitted = true
87
+ output.push({type: "session", sessionId: recognized.sessionId})
88
+ }
89
+ return output
90
+ }
91
+
92
+ /** @param {unknown} record @param {KimiRecordState | undefined} state @returns {KimiRecognition} */
93
+ function recognizeKimiRecord(record, state) {
94
+ if (!isRecord(record)) return emptyRecognition()
95
+ if (record.role === "assistant" && exactOptionalKeys(record, ["role"], ["content", "tool_calls"])) {
96
+ /** @type {WorkerEvent[]} */
97
+ const events = []
98
+ if (record.content !== undefined) {
99
+ if (typeof record.content !== "string") return emptyRecognition()
100
+ events.push({type: "text-delta", text: record.content, streamId: "kimi:assistant"})
101
+ }
102
+ if (record.tool_calls !== undefined) {
103
+ if (!Array.isArray(record.tool_calls)) return emptyRecognition()
104
+ for (const toolCall of record.tool_calls) {
105
+ const parsed = parseToolCall(toolCall)
106
+ if (parsed === undefined) return emptyRecognition()
107
+ state?.tools.set(parsed.id, parsed.name)
108
+ events.push({type: "tool", phase: "started", name: parsed.name, key: `tool:${parsed.id}`})
109
+ }
110
+ }
111
+ if (events.length === 0) return emptyRecognition()
112
+ return {trusted: true, sessionId: undefined, events: withStarted(events, state)}
113
+ }
114
+ if (record.role === "tool" && exactKeys(record, ["role", "tool_call_id", "content"])
115
+ && typeof record.tool_call_id === "string" && TOOL_ID_PATTERN.test(record.tool_call_id)
116
+ && typeof record.content === "string") {
117
+ const name = state?.tools.get(record.tool_call_id)
118
+ if (name === undefined || state === undefined) return emptyRecognition()
119
+ state.tools.delete(record.tool_call_id)
120
+ return {
121
+ trusted: true,
122
+ sessionId: undefined,
123
+ events: withStarted([{type: "tool", phase: "finished", name, key: `tool:${record.tool_call_id}`}], state)
124
+ }
125
+ }
126
+ if (record.role === "meta" && record.type === "session.resume_hint"
127
+ && exactKeys(record, ["role", "type", "session_id", "command", "content"])
128
+ && typeof record.session_id === "string" && SESSION_PATTERN.test(record.session_id)
129
+ && typeof record.command === "string" && typeof record.content === "string") {
130
+ return {trusted: true, sessionId: record.session_id, events: withStarted([], state)}
131
+ }
132
+ return emptyRecognition()
133
+ }
134
+
135
+ /** @param {unknown} value @returns {{id: string, name: string} | undefined} */
136
+ function parseToolCall(value) {
137
+ if (!isRecord(value) || !exactKeys(value, ["type", "id", "function"])
138
+ || value.type !== "function" || typeof value.id !== "string" || !TOOL_ID_PATTERN.test(value.id)
139
+ || !isRecord(value.function) || !exactKeys(value.function, ["name", "arguments"])
140
+ || typeof value.function.name !== "string" || !TOOL_NAME_PATTERN.test(value.function.name)
141
+ || typeof value.function.arguments !== "string") return undefined
142
+ return {id: value.id, name: value.function.name}
143
+ }
144
+
145
+ /** @param {WorkerEvent[]} events @param {KimiRecordState | undefined} state @returns {WorkerEvent[]} */
146
+ function withStarted(events, state) {
147
+ if (state === undefined || state.started) return events
148
+ state.started = true
149
+ return [{type: "lifecycle", phase: "started", summary: "Kimi worker started"}, ...events]
150
+ }
151
+ /** @returns {KimiRecognition} */
152
+ function emptyRecognition() { return {trusted: false, sessionId: undefined, events: []} }
153
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
154
+ function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
155
+ /** @param {Record<string, unknown>} value @param {readonly string[]} expected @returns {boolean} */
156
+ function exactKeys(value, expected) {
157
+ const actual = Object.keys(value).sort()
158
+ const wanted = [...expected].sort()
159
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index])
160
+ }
161
+ /** @param {Record<string, unknown>} value @param {readonly string[]} required @param {readonly string[]} optional @returns {boolean} */
162
+ function exactOptionalKeys(value, required, optional) {
163
+ const actual = Object.keys(value)
164
+ return required.every((key) => actual.includes(key)) && actual.every((key) => required.includes(key) || optional.includes(key))
165
+ }
@@ -2,16 +2,16 @@
2
2
 
3
3
  import {PROVIDERS} from "../providers/index.js"
4
4
 
5
- const COMMAND_PATTERN = /^\/code(?:@[A-Za-z0-9_]+)?\s+(codex|claude|opencode)\s+(\S[\s\S]*)$/u
5
+ const COMMAND_PATTERN = /^\/code(?:@[A-Za-z0-9_]+)?\s+(codex|claude|kimi|opencode)\s+(\S[\s\S]*)$/u
6
6
  const EVIDENCE_PATTERN = /^\/evidence(?:@[A-Za-z0-9_]+)?\s+(evidence_[A-Za-z0-9_-]{43})\s+(bytes|lines)\s+(\d+):(\d+)$/u
7
7
 
8
8
  /**
9
- * @typedef {{provider: "codex" | "claude" | "opencode", prompt: string}} CodeCommand
9
+ * @typedef {{provider: "codex" | "claude" | "kimi" | "opencode", prompt: string}} CodeCommand
10
10
  */
11
11
 
12
12
  /**
13
13
  * Parse the exact ingress command grammar:
14
- * `/code[optional @bot] <codex|claude|opencode> <nonblank prompt>`
14
+ * `/code[optional @bot] <codex|claude|kimi|opencode> <nonblank prompt>`
15
15
  * @param {string} text
16
16
  * @returns {CodeCommand | null}
17
17
  */
@@ -24,7 +24,7 @@ export function parseCodeCommand(text) {
24
24
  if (prompt.length === 0) return null
25
25
  if (!PROVIDERS.includes(/** @type {(typeof PROVIDERS)[number]} */ (providerName))) return null
26
26
  return {
27
- provider: /** @type {"codex" | "claude" | "opencode"} */ (providerName),
27
+ provider: /** @type {"codex" | "claude" | "kimi" | "opencode"} */ (providerName),
28
28
  prompt
29
29
  }
30
30
  }
@@ -139,6 +139,7 @@ export async function resolveIngressEnvironment(source, operations = {}) {
139
139
  * host: string,
140
140
  * maxConcurrentWorkers: number,
141
141
  * toolMessages: boolean,
142
+ * requireCodexIsolation: boolean,
142
143
  * telegramRequestTimeoutMs: number,
143
144
  * updateIdCapacity: number,
144
145
  * updateIdTtlMs: number
@@ -166,6 +167,9 @@ export function parseIngressConfig(environment) {
166
167
  DEFAULT_MAX_CONCURRENT_WORKERS
167
168
  )
168
169
  const toolMessages = parseStrictBoolean(environment.THREADWIRE_TOOL_MESSAGES, "THREADWIRE_TOOL_MESSAGES", false)
170
+ const requireCodexIsolation = parseStrictBoolean(
171
+ environment.THREADWIRE_REQUIRE_CODEX_ISOLATION, "THREADWIRE_REQUIRE_CODEX_ISOLATION", false
172
+ )
169
173
  const telegramRequestTimeoutMs = parseTelegramRequestTimeoutMs(environment)
170
174
  const updateIdCapacity = parsePositiveSafeInteger(
171
175
  environment.THREADWIRE_UPDATE_ID_CAPACITY,
@@ -187,6 +191,7 @@ export function parseIngressConfig(environment) {
187
191
  host,
188
192
  maxConcurrentWorkers,
189
193
  toolMessages,
194
+ requireCodexIsolation,
190
195
  telegramRequestTimeoutMs,
191
196
  updateIdCapacity,
192
197
  updateIdTtlMs,
@@ -234,9 +239,15 @@ export function buildProviderEnvironment(source) {
234
239
  /** @param {string} key */
235
240
  function isIngressSecretKey(key) {
236
241
  if (FILE_BACKED_SETTINGS.some((name) => key === `${name}_FILE`)) return true
242
+ if (key === "THREADWIRE_REQUIRE_CODEX_ISOLATION") return true
237
243
  if (
238
244
  key.startsWith("THREADWIRE_ISOLATED_RUNTIME_")
239
245
  || key.startsWith("THREADWIRE_MODEL_BROKER_")
246
+ || key.startsWith("THREADWIRE_KIMI_")
247
+ || key === "KIMI_MODEL_API_KEY"
248
+ || key === "KIMI_API_KEY"
249
+ || key === "MOONSHOT_API_KEY"
250
+ || key === "KIMI_CODE_HOME"
240
251
  || key === "THREADWIRE_RELAY_WORKER_IMAGE"
241
252
  || key === "THREADWIRE_ALLOWED_WORKTREE_ROOTS"
242
253
  || key === "THREADWIRE_WORKTREE_VOLUME"
@@ -6,6 +6,7 @@ import {NoticeQueue} from "../notice-queue.js"
6
6
  import {createFetchTransport} from "../notifiers/fetch-transport.js"
7
7
  import {createTelegramSender} from "../notifiers/telegram.js"
8
8
  import {createProvider} from "../providers/index.js"
9
+ import {kimiSessionEnvelopeId} from "../providers/kimi.js"
9
10
  import {Relay} from "../relay.js"
10
11
  import {runWorker} from "../run-worker.js"
11
12
  import {WorkerControl} from "../worker-control.js"
@@ -15,7 +16,7 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
15
16
 
16
17
  /**
17
18
  * @typedef {{platform: "telegram", chatId: string, threadId: number | null}} TelegramTarget
18
- * @typedef {{provider: "codex" | "claude" | "opencode", prompt: string, target: TelegramTarget, senderId: string} | {type: "evidence", request: unknown, target: TelegramTarget, senderId: string}} AcceptedJob
19
+ * @typedef {{provider: "codex" | "claude" | "kimi" | "opencode", prompt: string, target: TelegramTarget, senderId: string} | {type: "evidence", request: unknown, target: TelegramTarget, senderId: string}} AcceptedJob
19
20
  * @typedef {{
20
21
  * createProvider?: typeof createProvider,
21
22
  * createFetchTransport?: typeof createFetchTransport,
@@ -30,6 +31,7 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
30
31
  * activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
31
32
  * evidenceStore?: import("../evidence-store.js").EvidenceStore,
32
33
  * isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
34
+ * kimiIsolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
33
35
  * onWorkerFailure?: (message: string) => void,
34
36
  * onWorkerSettled?: () => void
35
37
  * }} IngressDependencies
@@ -88,12 +90,19 @@ export function interpretUpdate(update, config) {
88
90
  * spawn rejects (HTTP 500). Failure after spawn is observed via
89
91
  * onWorkerFailure without empty-swallowing the error.
90
92
  * @param {AcceptedJob} job
91
- * @param {{botToken: string, telegramRequestTimeoutMs?: number, toolMessages?: boolean}} config
93
+ * @param {{botToken: string, telegramRequestTimeoutMs?: number, toolMessages?: boolean, requireCodexIsolation?: boolean}} config
92
94
  * @param {IngressDependencies} [dependencies]
93
95
  * @returns {Promise<void>}
94
96
  */
95
97
  export async function dispatchWorker(job, config, dependencies = {}) {
96
98
  if ("type" in job) throw new Error("Worker dispatch requires a worker job")
99
+ if (job.provider === "kimi" && dependencies.kimiIsolatedRuntimeClient === undefined) {
100
+ throw new Error("Kimi isolated runtime is required")
101
+ }
102
+ if (job.provider === "codex" && config.requireCodexIsolation === true
103
+ && dependencies.isolatedRuntimeClient === undefined) {
104
+ throw new Error("Codex isolated runtime is required")
105
+ }
97
106
  const createProviderImpl = dependencies.createProvider ?? createProvider
98
107
  const createFetchTransportImpl = dependencies.createFetchTransport ?? createFetchTransport
99
108
  const createTelegramSenderImpl = dependencies.createTelegramSender ?? createTelegramSender
@@ -108,6 +117,18 @@ export async function dispatchWorker(job, config, dependencies = {}) {
108
117
  {provider: job.provider},
109
118
  dependencies.workspaceProfileOperations
110
119
  )
120
+ const isolatedRuntimeClient = job.provider === "kimi"
121
+ ? dependencies.kimiIsolatedRuntimeClient
122
+ : job.provider === "codex" ? dependencies.isolatedRuntimeClient : undefined
123
+ const earlyKimiPreflight = job.provider === "kimi"
124
+ ? await /** @type {NonNullable<typeof isolatedRuntimeClient>} */ (isolatedRuntimeClient).preflight({
125
+ provider: "kimi",
126
+ profile: workspace.profile,
127
+ repositoryRoot: workspace.repositoryRoot,
128
+ cwd: workspace.cwd,
129
+ providerArguments: []
130
+ })
131
+ : undefined
111
132
 
112
133
  const provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment)
113
134
  dependencies.activity?.recordWorkspace(
@@ -137,30 +158,32 @@ export async function dispatchWorker(job, config, dependencies = {}) {
137
158
  try {
138
159
  await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
139
160
 
140
- if (job.provider === "codex" && dependencies.isolatedRuntimeClient !== undefined) {
161
+ if (isolatedRuntimeClient !== undefined) {
141
162
  evidenceTransferred = true
142
163
  try {
143
- const preflight = await dependencies.isolatedRuntimeClient.preflight({
144
- provider: "codex",
164
+ const preflight = earlyKimiPreflight ?? await isolatedRuntimeClient.preflight({
165
+ provider: job.provider,
145
166
  profile: workspace.profile,
146
167
  repositoryRoot: workspace.repositoryRoot,
147
168
  cwd: workspace.cwd,
148
169
  providerArguments: []
149
170
  })
150
- const exitCode = await dependencies.isolatedRuntimeClient.run({
171
+ const exitCode = await isolatedRuntimeClient.run({
151
172
  preflightId: preflight.preflightId,
152
173
  prompt: job.prompt,
153
174
  providerArguments: [],
154
175
  ...(preflight.deadline === undefined ? {} : {deadline: preflight.deadline}),
155
176
  onEvent: async (event) => control.accept(event),
156
177
  onRecord: async (record) => {
157
- const id = provider.sessionId(record)
178
+ const id = job.provider === "kimi" ? kimiSessionEnvelopeId(record) : provider.sessionId(record)
158
179
  if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
159
180
  },
160
- onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
161
- onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
181
+ ...(job.provider === "kimi" ? {} : {
182
+ onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
183
+ onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
184
+ })
162
185
  })
163
- if (exitCode !== 0) throw new Error(`Isolated Codex worker exited with status ${exitCode}`)
186
+ if (exitCode !== 0) throw new Error(`Isolated ${job.provider === "kimi" ? "Kimi" : "Codex"} worker exited with status ${exitCode}`)
164
187
  await control.close()
165
188
  if (evidence !== undefined) {
166
189
  await evidence.finalize()
@@ -145,7 +145,8 @@ async function handleRequest(request, response, config, dependencies) {
145
145
  await dispatchWorker(job, {
146
146
  botToken: config.botToken,
147
147
  telegramRequestTimeoutMs: config.telegramRequestTimeoutMs,
148
- toolMessages: config.toolMessages
148
+ toolMessages: config.toolMessages,
149
+ requireCodexIsolation: config.requireCodexIsolation
149
150
  }, {
150
151
  ...dependencies,
151
152
  onWorkerSettled: () => {
@@ -5,7 +5,7 @@ import {ActivityLog} from "./activity-log.js"
5
5
  import {EvidenceStore} from "./evidence-store.js"
6
6
  import {buildProviderEnvironment, parseIngressConfig, resolveIngressEnvironment} from "./telegram-ingress/config.js"
7
7
  import {createWebhookHandler} from "./telegram-ingress/http.js"
8
- import {isolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
8
+ import {isolatedRuntimeClientFromEnvironment, kimiIsolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
9
9
 
10
10
  /**
11
11
  * Start the standalone Threadwire Telegram webhook service.
@@ -35,10 +35,14 @@ export async function startTelegramWebhook(options = {}) {
35
35
  const providerEnvironment = buildProviderEnvironment(
36
36
  options.providerEnvironment ?? environment
37
37
  )
38
- const isolatedConfigured = environment.THREADWIRE_ISOLATED_RUNTIME_URL !== undefined
39
- || environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN !== undefined
38
+ const isolatedConfigured = typeof environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN === "string"
39
+ && environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
40
40
  const isolatedRuntimeClient = options.handlerDependencies?.isolatedRuntimeClient
41
41
  ?? (isolatedConfigured ? isolatedRuntimeClientFromEnvironment(environment) : undefined)
42
+ const kimiIsolatedConfigured = typeof environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN === "string"
43
+ && environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
44
+ const kimiIsolatedRuntimeClient = options.handlerDependencies?.kimiIsolatedRuntimeClient
45
+ ?? (kimiIsolatedConfigured ? kimiIsolatedRuntimeClientFromEnvironment(environment) : undefined)
42
46
  const activity = options.activity ?? options.handlerDependencies?.activity ?? new ActivityLog("/var/lib/threadwire/activity/threadwire.jsonl")
43
47
  const evidenceStore = options.evidenceStore ?? options.handlerDependencies?.evidenceStore ?? await EvidenceStore.open({
44
48
  root: config.evidenceRoot ?? "/var/lib/threadwire/evidence"
@@ -50,6 +54,7 @@ export async function startTelegramWebhook(options = {}) {
50
54
  evidenceStore,
51
55
  providerEnvironment,
52
56
  ...(isolatedRuntimeClient === undefined ? {} : {isolatedRuntimeClient}),
57
+ ...(kimiIsolatedRuntimeClient === undefined ? {} : {kimiIsolatedRuntimeClient}),
53
58
  onOperationalError,
54
59
  onWorkerFailure
55
60
  })
@@ -14,7 +14,7 @@ const SOURCE_IDENTITY_PATTERN = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/u
14
14
  const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u
15
15
 
16
16
  /**
17
- * @typedef {{repositoryRoot: string, cwd: string, providers: ("codex" | "claude" | "opencode")[]}} WorkspaceProfile
17
+ * @typedef {{repositoryRoot: string, cwd: string, providers: ("codex" | "claude" | "kimi" | "opencode")[]}} WorkspaceProfile
18
18
  * @typedef {{version: 1, defaultProfile: string, profiles: Record<string, WorkspaceProfile>}} WorkspaceProfilesConfig
19
19
  * @typedef {{profile: string, repositoryRoot: string, cwd: string, revision: string, sourceIdentity: string}} ResolvedWorkspaceProfile
20
20
  * @typedef {{repositoryRoot: string, revision: string, sourceIdentity: string}} GitProvenance
@@ -52,7 +52,7 @@ export function parseWorkspaceProfiles(value) {
52
52
 
53
53
  /**
54
54
  * Resolve a reviewed workspace profile to a validated in-container workspace.
55
- * @param {{provider: "codex" | "claude" | "opencode", profile?: string}} selection
55
+ * @param {{provider: "codex" | "claude" | "kimi" | "opencode", profile?: string}} selection
56
56
  * @param {WorkspaceProfileOperations} [operations]
57
57
  * @returns {Promise<ResolvedWorkspaceProfile>}
58
58
  */
@@ -137,7 +137,7 @@ async function readSourceIdentity(repositoryRoot, cwd) {
137
137
  /** @param {unknown} value */
138
138
  function parseProviders(value) {
139
139
  if (!Array.isArray(value) || value.length === 0) throw new Error("Workspace profile configuration is invalid")
140
- /** @type {("codex" | "claude" | "opencode")[]} */
140
+ /** @type {("codex" | "claude" | "kimi" | "opencode")[]} */
141
141
  const providers = []
142
142
  for (const entry of value) {
143
143
  if (typeof entry !== "string") {
@@ -5,7 +5,7 @@
5
5
  "container-runtime": {
6
6
  "repositoryRoot": "/workspace/threadwire",
7
7
  "cwd": "/workspace/threadwire",
8
- "providers": ["codex"]
8
+ "providers": ["codex", "kimi"]
9
9
  }
10
10
  }
11
11
  }