@junwu168/openshell 0.1.2 → 0.1.4

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 (61) hide show
  1. package/dist/cli/openshell.js +4 -0
  2. package/dist/core/audit/log-store.js +1 -1
  3. package/dist/core/orchestrator.d.ts +2 -2
  4. package/dist/core/orchestrator.js +3 -3
  5. package/dist/core/result.d.ts +1 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/opencode/plugin.d.ts +1 -1
  9. package/dist/opencode/plugin.js +8 -8
  10. package/package.json +6 -1
  11. package/.claude/settings.local.json +0 -15
  12. package/bun.lock +0 -368
  13. package/docs/superpowers/notes/2026-03-25-opencode-remote-tools-handoff.md +0 -81
  14. package/docs/superpowers/notes/2026-03-26-openshell-pre-release-review.md +0 -174
  15. package/docs/superpowers/plans/2026-03-25-opencode-remote-tools.md +0 -1656
  16. package/docs/superpowers/plans/2026-03-25-server-registry-cli.md +0 -54
  17. package/docs/superpowers/plans/2026-03-26-config-backed-credential-registry.md +0 -494
  18. package/docs/superpowers/plans/2026-03-26-openshell-release-prep.md +0 -639
  19. package/docs/superpowers/specs/2026-03-25-opencode-remote-tools-design.md +0 -378
  20. package/docs/superpowers/specs/2026-03-26-config-backed-credential-registry-design.md +0 -272
  21. package/docs/superpowers/specs/2026-03-26-openshell-release-prep-design.md +0 -197
  22. package/examples/opencode-local/opencode.json +0 -19
  23. package/scripts/openshell.ts +0 -3
  24. package/scripts/server-registry.ts +0 -3
  25. package/src/cli/openshell.ts +0 -60
  26. package/src/cli/server-registry.ts +0 -476
  27. package/src/core/audit/git-audit-repo.ts +0 -42
  28. package/src/core/audit/log-store.ts +0 -20
  29. package/src/core/audit/redact.ts +0 -4
  30. package/src/core/contracts.ts +0 -51
  31. package/src/core/orchestrator.ts +0 -1082
  32. package/src/core/patch.ts +0 -11
  33. package/src/core/paths.ts +0 -32
  34. package/src/core/policy.ts +0 -30
  35. package/src/core/registry/server-registry.ts +0 -505
  36. package/src/core/result.ts +0 -16
  37. package/src/core/ssh/ssh-runtime.ts +0 -355
  38. package/src/index.ts +0 -3
  39. package/src/opencode/plugin.ts +0 -242
  40. package/src/product/install.ts +0 -43
  41. package/src/product/opencode-config.ts +0 -118
  42. package/src/product/uninstall.ts +0 -47
  43. package/src/product/workspace-tracker.ts +0 -69
  44. package/tests/integration/fake-ssh-server.ts +0 -97
  45. package/tests/integration/install-lifecycle.test.ts +0 -85
  46. package/tests/integration/orchestrator.test.ts +0 -767
  47. package/tests/integration/ssh-runtime.test.ts +0 -122
  48. package/tests/unit/audit.test.ts +0 -221
  49. package/tests/unit/build-layout.test.ts +0 -28
  50. package/tests/unit/opencode-config.test.ts +0 -100
  51. package/tests/unit/opencode-plugin.test.ts +0 -358
  52. package/tests/unit/openshell-cli.test.ts +0 -60
  53. package/tests/unit/paths.test.ts +0 -64
  54. package/tests/unit/plugin-export.test.ts +0 -10
  55. package/tests/unit/policy.test.ts +0 -53
  56. package/tests/unit/release-docs.test.ts +0 -31
  57. package/tests/unit/result.test.ts +0 -28
  58. package/tests/unit/server-registry-cli.test.ts +0 -673
  59. package/tests/unit/server-registry.test.ts +0 -452
  60. package/tests/unit/workspace-tracker.test.ts +0 -57
  61. package/tsconfig.json +0 -14
package/src/core/patch.ts DELETED
@@ -1,11 +0,0 @@
1
- import { applyPatch } from "diff"
2
-
3
- export const applyUnifiedPatch = (source: string, patch: string) => {
4
- const next = applyPatch(source, patch)
5
-
6
- if (next === false) {
7
- throw new Error("patch apply failed")
8
- }
9
-
10
- return next
11
- }
package/src/core/paths.ts DELETED
@@ -1,32 +0,0 @@
1
- import envPaths from "env-paths"
2
- import { mkdir } from "node:fs/promises"
3
- import { cwd } from "node:process"
4
-
5
- export const createRuntimePaths = (workspaceRoot: string) => {
6
- const openshellPaths = envPaths("openshell", { suffix: "" })
7
- const opencodePaths = envPaths("opencode", { suffix: "" })
8
-
9
- return {
10
- configDir: openshellPaths.config,
11
- dataDir: openshellPaths.data,
12
- globalRegistryFile: `${openshellPaths.config}/servers.json`,
13
- workspaceTrackerFile: `${openshellPaths.data}/workspaces.json`,
14
- opencodeConfigDir: opencodePaths.config,
15
- opencodeConfigFile: `${opencodePaths.config}/opencode.json`,
16
- workspaceRegistryDir: `${workspaceRoot}/.open-code`,
17
- workspaceRegistryFile: `${workspaceRoot}/.open-code/servers.json`,
18
- auditLogFile: `${openshellPaths.data}/audit/actions.jsonl`,
19
- auditRepoDir: `${openshellPaths.data}/audit/repo`,
20
- }
21
- }
22
-
23
- export const runtimePaths = createRuntimePaths(cwd())
24
-
25
- export const workspaceRegistryFile = (workspaceRoot: string) =>
26
- `${workspaceRoot}/.open-code/servers.json`
27
-
28
- export const ensureRuntimeDirs = async () => {
29
- await mkdir(`${runtimePaths.dataDir}/audit`, { recursive: true })
30
- await mkdir(runtimePaths.auditRepoDir, { recursive: true })
31
- await mkdir(runtimePaths.configDir, { recursive: true })
32
- }
@@ -1,30 +0,0 @@
1
- const SAFE_COMMANDS = new Set(["cat", "grep", "find", "ls", "pwd", "uname", "df", "free", "ps"])
2
- const MIDDLEWARE_COMMANDS = new Set([
3
- "psql",
4
- "mysql",
5
- "redis-cli",
6
- "kubectl",
7
- "docker",
8
- "helm",
9
- "aws",
10
- "gcloud",
11
- "az",
12
- ])
13
- const SHELL_META = ["|", ">", "<", ";", "&&", "||", "$(", "`"]
14
-
15
- export const classifyRemoteExec = (command: string) => {
16
- const trimmed = command.trim()
17
- if (!trimmed) return { decision: "reject", reason: "empty command" } as const
18
- if (SHELL_META.some((token) => trimmed.includes(token))) {
19
- return { decision: "approval-required", reason: "shell composition" } as const
20
- }
21
-
22
- const [binary, subcommand] = trimmed.split(/\s+/)
23
- if (MIDDLEWARE_COMMANDS.has(binary)) {
24
- return { decision: "approval-required", reason: "middleware command" } as const
25
- }
26
- if (SAFE_COMMANDS.has(binary) || (binary === "systemctl" && subcommand === "status")) {
27
- return { decision: "auto-allow", reason: "safe inspection command" } as const
28
- }
29
- return { decision: "approval-required", reason: "unknown command" } as const
30
- }
@@ -1,505 +0,0 @@
1
- import { link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
2
- import { execFile } from "node:child_process"
3
- import { randomUUID } from "node:crypto"
4
- import { dirname } from "node:path"
5
- import { promisify } from "node:util"
6
-
7
- export type ServerMetadataValue = string | number | boolean | null
8
-
9
- export interface PasswordAuthRecord {
10
- kind: "password"
11
- secret: string
12
- }
13
-
14
- export interface PrivateKeyAuthRecord {
15
- kind: "privateKey"
16
- privateKeyPath: string
17
- passphrase?: string
18
- }
19
-
20
- export interface CertificateAuthRecord {
21
- kind: "certificate"
22
- certificatePath: string
23
- privateKeyPath: string
24
- passphrase?: string
25
- }
26
-
27
- export type ServerAuthRecord =
28
- | PasswordAuthRecord
29
- | PrivateKeyAuthRecord
30
- | CertificateAuthRecord
31
-
32
- export interface ServerRecord {
33
- id: string
34
- host: string
35
- port: number
36
- username: string
37
- labels?: string[]
38
- groups?: string[]
39
- metadata?: Record<string, ServerMetadataValue>
40
- auth: ServerAuthRecord
41
- }
42
-
43
- export type RegistryScope = "global" | "workspace"
44
-
45
- export type ResolvedServerRecord = ServerRecord & {
46
- scope: RegistryScope
47
- shadowingGlobal?: boolean
48
- workspaceRoot?: string
49
- }
50
-
51
- export class RegistryRecordValidationError extends Error {
52
- readonly code = "REGISTRY_RECORD_INVALID" as const
53
-
54
- constructor(
55
- readonly file: string,
56
- readonly index: number,
57
- message: string,
58
- ) {
59
- super(`Invalid registry record in ${file} at index ${index}: ${message}`)
60
- this.name = "RegistryRecordValidationError"
61
- }
62
- }
63
-
64
- export interface ServerRegistry {
65
- list(): Promise<ResolvedServerRecord[]>
66
- resolve(id: string): Promise<ResolvedServerRecord | null>
67
- upsert(scope: RegistryScope, record: ServerRecord): Promise<void>
68
- remove(scope: RegistryScope, id: string): Promise<boolean>
69
- listRaw(scope: RegistryScope): Promise<ServerRecord[]>
70
- }
71
-
72
- type FileLockOptions = {
73
- getProcessStartTime?: (pid: number) => Promise<number | null>
74
- retryMs?: number
75
- timeoutMs?: number
76
- }
77
-
78
- export interface CreateServerRegistryOptions {
79
- globalRegistryFile: string
80
- workspaceRegistryFile: string
81
- workspaceRoot: string
82
- lockOptions?: FileLockOptions
83
- }
84
-
85
- const isRecordObject = (value: unknown): value is Record<string, unknown> =>
86
- typeof value === "object" && value !== null && !Array.isArray(value)
87
-
88
- const validateString = (value: unknown, field: string, file: string, index: number) => {
89
- if (typeof value !== "string" || value.trim() === "") {
90
- throw new RegistryRecordValidationError(file, index, `${field} must be a non-empty string`)
91
- }
92
-
93
- return value
94
- }
95
-
96
- const validateStringArray = (
97
- value: unknown,
98
- field: string,
99
- file: string,
100
- index: number,
101
- ): string[] | undefined => {
102
- if (value === undefined) {
103
- return undefined
104
- }
105
-
106
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
107
- throw new RegistryRecordValidationError(file, index, `${field} must be an array of non-empty strings`)
108
- }
109
-
110
- return value
111
- }
112
-
113
- const validateMetadata = (
114
- value: unknown,
115
- file: string,
116
- index: number,
117
- ): Record<string, ServerMetadataValue> | undefined => {
118
- if (value === undefined) {
119
- return undefined
120
- }
121
-
122
- if (!isRecordObject(value)) {
123
- throw new RegistryRecordValidationError(file, index, "metadata must be an object")
124
- }
125
-
126
- for (const [key, entry] of Object.entries(value)) {
127
- if (typeof key !== "string" || key.trim() === "") {
128
- throw new RegistryRecordValidationError(file, index, "metadata keys must be non-empty strings")
129
- }
130
-
131
- if (
132
- !(
133
- typeof entry === "string" ||
134
- typeof entry === "number" ||
135
- typeof entry === "boolean" ||
136
- entry === null
137
- )
138
- ) {
139
- throw new RegistryRecordValidationError(file, index, "metadata values must be strings, numbers, booleans, or null")
140
- }
141
- }
142
-
143
- return value as Record<string, ServerMetadataValue>
144
- }
145
-
146
- const validateAuth = (auth: unknown, file: string, index: number): ServerAuthRecord => {
147
- if (!isRecordObject(auth)) {
148
- throw new RegistryRecordValidationError(file, index, "auth must be an object")
149
- }
150
-
151
- const kind = validateString(auth.kind, "auth.kind", file, index)
152
- switch (kind) {
153
- case "password":
154
- return {
155
- kind,
156
- secret: validateString(auth.secret, "auth.secret", file, index),
157
- }
158
- case "privateKey":
159
- return {
160
- kind,
161
- privateKeyPath: validateString(auth.privateKeyPath, "auth.privateKeyPath", file, index),
162
- ...(typeof auth.passphrase === "string" ? { passphrase: auth.passphrase } : {}),
163
- }
164
- case "certificate":
165
- return {
166
- kind,
167
- certificatePath: validateString(auth.certificatePath, "auth.certificatePath", file, index),
168
- privateKeyPath: validateString(auth.privateKeyPath, "auth.privateKeyPath", file, index),
169
- ...(typeof auth.passphrase === "string" ? { passphrase: auth.passphrase } : {}),
170
- }
171
- default:
172
- throw new RegistryRecordValidationError(file, index, `unsupported auth kind: ${kind}`)
173
- }
174
- }
175
-
176
- const validateRecord = (record: unknown, file: string, index: number): ServerRecord => {
177
- if (!isRecordObject(record)) {
178
- throw new RegistryRecordValidationError(file, index, "record must be an object")
179
- }
180
-
181
- const labels = validateStringArray(record.labels, "labels", file, index)
182
- const groups = validateStringArray(record.groups, "groups", file, index)
183
- const metadata = validateMetadata(record.metadata, file, index)
184
-
185
- return {
186
- id: validateString(record.id, "id", file, index),
187
- host: validateString(record.host, "host", file, index),
188
- port: (() => {
189
- const port = record.port
190
- if (typeof port !== "number" || !Number.isInteger(port) || port <= 0) {
191
- throw new RegistryRecordValidationError(file, index, "port must be a positive integer")
192
- }
193
- return port
194
- })(),
195
- username: validateString(record.username, "username", file, index),
196
- ...(labels ? { labels } : {}),
197
- ...(groups ? { groups } : {}),
198
- ...(metadata ? { metadata } : {}),
199
- auth: validateAuth(record.auth, file, index),
200
- }
201
- }
202
-
203
- const parseRecords = (raw: string, file: string) => {
204
- const parsed = JSON.parse(raw) as unknown
205
- if (!Array.isArray(parsed)) {
206
- throw new Error(`Invalid registry file: ${file}`)
207
- }
208
-
209
- return parsed.map((record, index) => validateRecord(record, file, index))
210
- }
211
-
212
- const buildResolvedRecord = (
213
- record: ServerRecord,
214
- scope: RegistryScope,
215
- workspaceRoot: string,
216
- shadowingGlobal = false,
217
- ): ResolvedServerRecord => ({
218
- ...record,
219
- scope,
220
- ...(scope === "workspace" ? { workspaceRoot } : {}),
221
- ...(shadowingGlobal ? { shadowingGlobal: true } : {}),
222
- })
223
-
224
- export const createServerRegistry = ({
225
- globalRegistryFile,
226
- workspaceRegistryFile,
227
- workspaceRoot,
228
- lockOptions,
229
- }: CreateServerRegistryOptions): ServerRegistry => {
230
- const execFileAsync = promisify(execFile)
231
- const resolveProcessStartTime = lockOptions?.getProcessStartTime
232
- const lockRetryMs = lockOptions?.retryMs ?? 10
233
- const lockTimeoutMs = lockOptions?.timeoutMs ?? 5_000
234
- const fileQueues = new Map<string, Promise<void>>()
235
-
236
- const scopeFile = (scope: RegistryScope) =>
237
- scope === "global" ? globalRegistryFile : workspaceRegistryFile
238
-
239
- const getFileQueue = (file: string) => fileQueues.get(file) ?? Promise.resolve()
240
-
241
- const enqueueWrite = <T>(file: string, operation: () => Promise<T>) => {
242
- const next = getFileQueue(file).then(operation)
243
- fileQueues.set(
244
- file,
245
- next.then(
246
- () => undefined,
247
- () => undefined,
248
- ),
249
- )
250
-
251
- return next
252
- }
253
-
254
- const waitForWrites = async (...files: string[]) => {
255
- await Promise.all(files.map((file) => getFileQueue(file)))
256
- }
257
-
258
- const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
259
-
260
- const isProcessAlive = (pid: number) => {
261
- try {
262
- process.kill(pid, 0)
263
- return true
264
- } catch (error: unknown) {
265
- const code = (error as NodeJS.ErrnoException).code
266
- if (code === "ESRCH") {
267
- return false
268
- }
269
- if (code === "EPERM") {
270
- return true
271
- }
272
- throw error
273
- }
274
- }
275
-
276
- const getProcessStartTime = async (pid: number) => {
277
- if (resolveProcessStartTime) {
278
- return resolveProcessStartTime(pid)
279
- }
280
-
281
- try {
282
- const { stdout } = await execFileAsync("ps", ["-o", "etimes=", "-p", String(pid)])
283
- const elapsedSecondsText = stdout.trim()
284
- if (!/^\d+$/.test(elapsedSecondsText)) {
285
- return null
286
- }
287
-
288
- const elapsedSeconds = Number.parseInt(elapsedSecondsText, 10)
289
- if (!Number.isFinite(elapsedSeconds)) {
290
- return null
291
- }
292
-
293
- return Date.now() - elapsedSeconds * 1_000
294
- } catch (error: unknown) {
295
- const exitCode = (error as { code?: unknown }).code
296
- if (exitCode === 1 || exitCode === "EPERM") {
297
- return null
298
- }
299
- throw error
300
- }
301
- }
302
-
303
- const tryReclaimAbandonedLock = async (lockFile: string) => {
304
- let raw: string
305
- try {
306
- raw = await readFile(lockFile, "utf8")
307
- } catch (error: unknown) {
308
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
309
- return false
310
- }
311
- throw error
312
- }
313
-
314
- let ownerPid: number | null = null
315
- let lockCreatedAt: number | null = null
316
- try {
317
- const parsed = JSON.parse(raw) as { pid?: unknown; createdAt?: unknown }
318
- ownerPid = typeof parsed.pid === "number" ? parsed.pid : null
319
- if (typeof parsed.createdAt === "string") {
320
- const parsedCreatedAt = Date.parse(parsed.createdAt)
321
- lockCreatedAt = Number.isNaN(parsedCreatedAt) ? null : parsedCreatedAt
322
- }
323
- } catch {
324
- return false
325
- }
326
-
327
- if (ownerPid === null) {
328
- return false
329
- }
330
-
331
- if (isProcessAlive(ownerPid)) {
332
- if (lockCreatedAt === null) {
333
- return false
334
- }
335
-
336
- const ownerStartedAt = await getProcessStartTime(ownerPid)
337
- if (ownerStartedAt === null || ownerStartedAt <= lockCreatedAt) {
338
- return false
339
- }
340
- }
341
-
342
- const reclaimedLockFile = `${lockFile}.reclaimed.${randomUUID()}`
343
- try {
344
- await rename(lockFile, reclaimedLockFile)
345
- } catch (error: unknown) {
346
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
347
- return false
348
- }
349
- throw error
350
- }
351
-
352
- await rm(reclaimedLockFile, { force: true })
353
- return true
354
- }
355
-
356
- const withFileLock = async <T>(file: string, operation: () => Promise<T>): Promise<T> => {
357
- const lockFile = `${file}.lock`
358
- const startedAt = Date.now()
359
- await mkdir(dirname(file), { recursive: true })
360
-
361
- while (true) {
362
- const pendingLockFile = `${lockFile}.${process.pid}.${randomUUID()}.pending`
363
- try {
364
- await writeFile(
365
- pendingLockFile,
366
- JSON.stringify({
367
- pid: process.pid,
368
- createdAt: new Date().toISOString(),
369
- }),
370
- )
371
-
372
- await link(pendingLockFile, lockFile)
373
-
374
- try {
375
- return await operation()
376
- } finally {
377
- await rm(lockFile, { force: true })
378
- }
379
- } catch (error: unknown) {
380
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
381
- throw error
382
- }
383
- } finally {
384
- await rm(pendingLockFile, { force: true })
385
- }
386
-
387
- if (await tryReclaimAbandonedLock(lockFile)) {
388
- continue
389
- }
390
-
391
- if (Date.now() - startedAt > lockTimeoutMs) {
392
- throw new Error(`Timed out waiting for registry lock: ${lockFile}`)
393
- }
394
-
395
- await sleep(lockRetryMs)
396
- }
397
- }
398
-
399
- const loadRaw = async (scope: RegistryScope): Promise<ServerRecord[]> => {
400
- const file = scopeFile(scope)
401
-
402
- try {
403
- const raw = await readFile(file, "utf8")
404
- return parseRecords(raw, file)
405
- } catch (error: unknown) {
406
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
407
- return []
408
- }
409
-
410
- throw error
411
- }
412
- }
413
-
414
- const saveRaw = async (scope: RegistryScope, records: ServerRecord[]) => {
415
- const file = scopeFile(scope)
416
- await mkdir(dirname(file), { recursive: true })
417
- const tempFile = `${file}.${process.pid}.${randomUUID()}.tmp`
418
-
419
- try {
420
- await writeFile(tempFile, JSON.stringify(records, null, 2))
421
- await rename(tempFile, file)
422
- } catch (error) {
423
- await rm(tempFile, { force: true })
424
- throw error
425
- }
426
- }
427
-
428
- const mergeRecords = (
429
- globalRecords: ServerRecord[],
430
- workspaceRecords: ServerRecord[],
431
- ): ResolvedServerRecord[] => {
432
- const merged = new Map<string, ResolvedServerRecord>()
433
- const order: string[] = []
434
- const globalIds = new Set<string>()
435
-
436
- for (const record of globalRecords) {
437
- globalIds.add(record.id)
438
- if (!merged.has(record.id)) {
439
- order.push(record.id)
440
- }
441
- merged.set(record.id, buildResolvedRecord(record, "global", workspaceRoot))
442
- }
443
-
444
- for (const record of workspaceRecords) {
445
- const shadowingGlobal = globalIds.has(record.id)
446
- if (!merged.has(record.id)) {
447
- order.push(record.id)
448
- }
449
- merged.set(record.id, buildResolvedRecord(record, "workspace", workspaceRoot, shadowingGlobal))
450
- }
451
-
452
- return order.map((id) => merged.get(id)!).filter(Boolean)
453
- }
454
-
455
- return {
456
- async list() {
457
- await waitForWrites(globalRegistryFile, workspaceRegistryFile)
458
- const [globalRecords, workspaceRecords] = await Promise.all([
459
- loadRaw("global"),
460
- loadRaw("workspace"),
461
- ])
462
- return mergeRecords(globalRecords, workspaceRecords)
463
- },
464
- async resolve(id) {
465
- await waitForWrites(globalRegistryFile, workspaceRegistryFile)
466
- const [globalRecords, workspaceRecords] = await Promise.all([
467
- loadRaw("global"),
468
- loadRaw("workspace"),
469
- ])
470
- return mergeRecords(globalRecords, workspaceRecords).find((record) => record.id === id) ?? null
471
- },
472
- async upsert(scope, record) {
473
- const file = scopeFile(scope)
474
- return enqueueWrite(file, async () =>
475
- withFileLock(file, async () => {
476
- const records = await loadRaw(scope)
477
- const next = records.filter((item) => item.id !== record.id)
478
- next.push(record)
479
- await saveRaw(scope, next)
480
- }),
481
- )
482
- },
483
- async remove(scope, id) {
484
- const file = scopeFile(scope)
485
- return enqueueWrite(file, async () =>
486
- withFileLock(file, async () => {
487
- const records = await loadRaw(scope)
488
- const next = records.filter((item) => item.id !== id)
489
-
490
- if (next.length === records.length) {
491
- return false
492
- }
493
-
494
- await saveRaw(scope, next)
495
- return true
496
- }),
497
- )
498
- },
499
- async listRaw(scope) {
500
- const file = scopeFile(scope)
501
- await waitForWrites(file)
502
- return loadRaw(scope)
503
- },
504
- }
505
- }
@@ -1,16 +0,0 @@
1
- import type { ToolPayload, ToolResult } from "./contracts"
2
-
3
- export const okResult = <T>(payload: ToolPayload<T>): ToolResult<T> => ({
4
- ...payload,
5
- status: "ok",
6
- })
7
-
8
- export const partialFailureResult = <T>(payload: ToolPayload<T>): ToolResult<T> => ({
9
- ...payload,
10
- status: "partial_failure",
11
- })
12
-
13
- export const errorResult = <T>(payload: ToolPayload<T>): ToolResult<T> => ({
14
- ...payload,
15
- status: "error",
16
- })