@coralai/sps-cli 0.68.0 → 0.69.1

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 (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/assembly/base.yml +23 -0
  3. package/dist/console/index.d.ts.map +1 -1
  4. package/dist/console/index.js +27 -0
  5. package/dist/console/index.js.map +1 -1
  6. package/dist/console/routes/plugins.d.ts.map +1 -1
  7. package/dist/console/routes/plugins.js +21 -2
  8. package/dist/console/routes/plugins.js.map +1 -1
  9. package/dist/console/serviceAuth.d.ts +6 -2
  10. package/dist/console/serviceAuth.d.ts.map +1 -1
  11. package/dist/console/serviceAuth.js +10 -4
  12. package/dist/console/serviceAuth.js.map +1 -1
  13. package/dist/console-assets/assets/{ShowcasePage-DyKTVaXk.js → ShowcasePage-BaaX3Cpt.js} +1 -1
  14. package/dist/console-assets/assets/{index-D-8ikIK_.css → index-BDmCjVjN.css} +1 -1
  15. package/dist/console-assets/assets/{index-BN0r9VlY.js → index-BgAaYxbH.js} +3 -3
  16. package/dist/console-assets/index.html +2 -2
  17. package/dist/plugin/manager.d.ts +6 -0
  18. package/dist/plugin/manager.d.ts.map +1 -1
  19. package/dist/plugin/manager.js +164 -26
  20. package/dist/plugin/manager.js.map +1 -1
  21. package/dist/plugin/patchFile.d.ts +27 -0
  22. package/dist/plugin/patchFile.d.ts.map +1 -0
  23. package/dist/plugin/patchFile.js +71 -0
  24. package/dist/plugin/patchFile.js.map +1 -0
  25. package/dist/providers/mcp/spsMcpServer.d.ts.map +1 -1
  26. package/dist/providers/mcp/spsMcpServer.js +7 -1
  27. package/dist/providers/mcp/spsMcpServer.js.map +1 -1
  28. package/dist/seams/attachments/index.d.ts +28 -0
  29. package/dist/seams/attachments/index.d.ts.map +1 -0
  30. package/dist/seams/attachments/index.js +72 -0
  31. package/dist/seams/attachments/index.js.map +1 -0
  32. package/dist/seams/compat-dsh/credentials.d.ts +28 -0
  33. package/dist/seams/compat-dsh/credentials.d.ts.map +1 -0
  34. package/dist/seams/compat-dsh/credentials.js +33 -0
  35. package/dist/seams/compat-dsh/credentials.js.map +1 -0
  36. package/dist/seams/compat-dsh/index.d.ts +26 -0
  37. package/dist/seams/compat-dsh/index.d.ts.map +1 -0
  38. package/dist/seams/compat-dsh/index.js +21 -0
  39. package/dist/seams/compat-dsh/index.js.map +1 -0
  40. package/dist/seams/compat-dsh/misc.d.ts +31 -0
  41. package/dist/seams/compat-dsh/misc.d.ts.map +1 -0
  42. package/dist/seams/compat-dsh/misc.js +41 -0
  43. package/dist/seams/compat-dsh/misc.js.map +1 -0
  44. package/dist/seams/compat-dsh/tools.d.ts +71 -0
  45. package/dist/seams/compat-dsh/tools.d.ts.map +1 -0
  46. package/dist/seams/compat-dsh/tools.js +92 -0
  47. package/dist/seams/compat-dsh/tools.js.map +1 -0
  48. package/dist/seams/compat-dsh/webserver.d.ts +43 -0
  49. package/dist/seams/compat-dsh/webserver.d.ts.map +1 -0
  50. package/dist/seams/compat-dsh/webserver.js +50 -0
  51. package/dist/seams/compat-dsh/webserver.js.map +1 -0
  52. package/dist/seams/credentials/index.d.ts +52 -0
  53. package/dist/seams/credentials/index.d.ts.map +1 -0
  54. package/dist/seams/credentials/index.js +33 -0
  55. package/dist/seams/credentials/index.js.map +1 -0
  56. package/dist/seams/credentials-local/index.d.ts +12 -0
  57. package/dist/seams/credentials-local/index.d.ts.map +1 -0
  58. package/dist/seams/credentials-local/index.js +93 -0
  59. package/dist/seams/credentials-local/index.js.map +1 -0
  60. package/dist/seams/subprocess/index.d.ts +34 -0
  61. package/dist/seams/subprocess/index.d.ts.map +1 -0
  62. package/dist/seams/subprocess/index.js +77 -0
  63. package/dist/seams/subprocess/index.js.map +1 -0
  64. package/dist/seams/tools/jsonSchemaToZod.d.ts +23 -0
  65. package/dist/seams/tools/jsonSchemaToZod.d.ts.map +1 -0
  66. package/dist/seams/tools/jsonSchemaToZod.js +67 -0
  67. package/dist/seams/tools/jsonSchemaToZod.js.map +1 -0
  68. package/dist/seams/webserver/index.d.ts +92 -0
  69. package/dist/seams/webserver/index.d.ts.map +1 -0
  70. package/dist/seams/webserver/index.js +100 -0
  71. package/dist/seams/webserver/index.js.map +1 -0
  72. package/package.json +1 -1
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @module seams/credentials
3
+ * @description **凭据引用 seam** —— 配置里存**引用**(名字),真值由 provider 持有。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 类型:**甲 · 工厂型**(进程里一个实现)。
8
+ *
9
+ * 🔴 **这个我们本来就该有,和兼容 dsh 无关。** 两个收益:
10
+ * ① **装配档里永远没有密文** —— 插件配置里现在是可以直接塞 key 的,
11
+ * 而装配档会被每个进程在启动时读、也显示在插件页上。
12
+ * ② **换密钥不用重挂插件** —— `apply(ctx, config)` 只在挂载那一刻读一次,
13
+ * 而引用是**每次操作现解**的。
14
+ *
15
+ * 形状抄 dsh(`ctx.credentials`):`resolve / describe / set / unset`。
16
+ * dsh 的原话:"configuration surfaces describe a reference without ever seeing its value."
17
+ */
18
+ import { Service } from '@deepseek-ai/cordis';
19
+ export const CREDENTIALS = 'sps.credentials';
20
+ /** 引用必须是 POSIX 标识符(= 环境变量名的形状)。 */
21
+ const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
+ export function credentialRef(value) {
23
+ if (!REF_PATTERN.test(value)) {
24
+ throw new TypeError(`凭据引用 "${value}" 必须匹配 ${String(REF_PATTERN)}`);
25
+ }
26
+ return value;
27
+ }
28
+ export default class CredentialsSeam extends Service {
29
+ constructor(ctx) {
30
+ super(ctx, CREDENTIALS);
31
+ }
32
+ }
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seams/credentials/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAgB,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAE7C,mCAAmC;AACnC,MAAM,WAAW,GAAG,0BAA0B,CAAC;AAI/C,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CAAC,SAAS,KAAK,UAAU,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,KAAsB,CAAC;AAChC,CAAC;AAkBD,MAAM,CAAC,OAAO,OAAgB,eAAgB,SAAQ,OAAO;IAC3D,YAAY,GAAY;QACtB,KAAK,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC1B,CAAC;CAWF"}
@@ -0,0 +1,12 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import CredentialsSeam, { type CredentialInfo, type CredentialRef, type ResolvedCredential } from '../credentials/index.js';
3
+ export declare const category = "\u51ED\u636E";
4
+ export declare const description = "\u672C\u5730\u51ED\u636E:\u5148\u8BFB\u8FDB\u7A0B\u73AF\u5883\u53D8\u91CF(\u8FD0\u7EF4\u6CE8\u5165\u7684,\u53EA\u8BFB),\u518D\u8BFB ~/.coral/env(\u53EF\u5199,0600)\u3002";
5
+ export default class LocalCredentials extends CredentialsSeam {
6
+ constructor(ctx: Context);
7
+ resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>;
8
+ describe(ref: CredentialRef): Promise<CredentialInfo>;
9
+ set(ref: CredentialRef, value: string): Promise<void>;
10
+ unset(ref: CredentialRef): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seams/credentials-local/index.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,eAAe,EAAE,EACtB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAEjC,eAAO,MAAM,QAAQ,iBAAO,CAAC;AAC7B,eAAO,MAAM,WAAW,8KAC6B,CAAC;AAwBtD,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,eAAe;gBAC/C,GAAG,EAAE,OAAO;IAIlB,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAOpE,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAWrD,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrD,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAS/C"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @module seams/credentials-local
3
+ * @description 本地凭据 provider:`process.env` → `~/.coral/env`。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 解析顺序(与 dsh 的 credentials-local 同构):
8
+ * ```
9
+ * 继承的 process.env 只读,**优先** —— 运维用 systemd/pm2 注进来的那层
10
+ * ~/.coral/env 可读可写 —— 我们持久化的那层
11
+ * ```
12
+ *
13
+ * 🔴 **process.env 优先且只读**:它是拉起方(systemd / 运维)给的,
14
+ * 在这里写一份同名的会造成"页面上改了、进程里没变"——
15
+ * 那是最难查的一类不一致。所以 describe 会把 `writable:false` 说出来。
16
+ *
17
+ * 🔴 **写走 `writeSecretFile`**(tmp 0600 → rename → chmod,顺序不能反)。
18
+ * 直接 writeFileSync 会先按 umask 落一个可被同组读的文件,再改权限 ——
19
+ * 中间那一瞬是真实敞口。
20
+ */
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { homedir } from 'node:os';
23
+ import { resolve } from 'node:path';
24
+ import { hardenSecretFile, writeSecretFile } from '../../infra/secretFile.js';
25
+ import CredentialsSeam from '../credentials/index.js';
26
+ export const category = '凭据';
27
+ export const description = '本地凭据:先读进程环境变量(运维注入的,只读),再读 ~/.coral/env(可写,0600)。';
28
+ function envFile() {
29
+ return resolve(process.env.SPS_CREDENTIALS_FILE ?? resolve(homedir(), '.coral', 'env'));
30
+ }
31
+ /** 读 `~/.coral/env`。它是 shell 风格(`export K=v` / `K=v`),不是 dotenv 规范。 */
32
+ function readEnvFile() {
33
+ const out = new Map();
34
+ const p = envFile();
35
+ if (!existsSync(p))
36
+ return out;
37
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
38
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
39
+ if (!m)
40
+ continue;
41
+ let v = (m[2] ?? '').trim();
42
+ // 去掉成对的引号;内容里的引号原样留着
43
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
44
+ v = v.slice(1, -1);
45
+ }
46
+ out.set(m[1], v);
47
+ }
48
+ return out;
49
+ }
50
+ export default class LocalCredentials extends CredentialsSeam {
51
+ constructor(ctx) {
52
+ super(ctx);
53
+ }
54
+ async resolve(ref) {
55
+ const fromEnv = process.env[ref];
56
+ if (fromEnv)
57
+ return { value: fromEnv, source: 'process.env' };
58
+ const v = readEnvFile().get(ref);
59
+ return v ? { value: v, source: envFile() } : undefined;
60
+ }
61
+ async describe(ref) {
62
+ // 🔴 绝不回值。这一层存在的全部理由就是"描述一个引用而不看见它"。
63
+ if (process.env[ref]) {
64
+ return { ref, configured: true, source: 'process.env', writable: false };
65
+ }
66
+ const v = readEnvFile().get(ref);
67
+ return v
68
+ ? { ref, configured: true, source: envFile(), writable: true }
69
+ : { ref, configured: false, writable: true };
70
+ }
71
+ async set(ref, value) {
72
+ if (process.env[ref]) {
73
+ throw new Error(`${ref} 由进程环境变量提供(拉起方注入的),这里改不了 —— 改了也会被它盖住`);
74
+ }
75
+ const p = envFile();
76
+ const lines = existsSync(p) ? readFileSync(p, 'utf8').split('\n') : [];
77
+ const re = new RegExp(`^\\s*(?:export\\s+)?${ref}\\s*=`);
78
+ const next = lines.filter((l) => !re.test(l));
79
+ next.push(`${ref}=${value}`);
80
+ writeSecretFile(p, `${next.filter((l, i) => l !== '' || i < next.length - 1).join('\n')}\n`);
81
+ }
82
+ async unset(ref) {
83
+ const p = envFile();
84
+ if (!existsSync(p))
85
+ return;
86
+ const re = new RegExp(`^\\s*(?:export\\s+)?${ref}\\s*=`);
87
+ const kept = readFileSync(p, 'utf8').split('\n').filter((l) => !re.test(l));
88
+ writeSecretFile(p, `${kept.join('\n').replace(/\n+$/, '')}\n`);
89
+ // 存量文件可能是 664:任一次写后顺手收紧(同 channels 那条既有判据)
90
+ hardenSecretFile(p);
91
+ }
92
+ }
93
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seams/credentials-local/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type Context, Service } from '@deepseek-ai/cordis';
2
+ export declare const SUBPROCESS = "sps.subprocess";
3
+ export interface SpawnSpec {
4
+ argv: readonly string[];
5
+ cwd: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ signal?: AbortSignal;
8
+ /** 收到取消后,等它自己退出多久才 SIGKILL。默认 5000。 */
9
+ graceMs?: number;
10
+ }
11
+ export interface SpawnResult {
12
+ code: number | null;
13
+ stdout: string;
14
+ stderr: string;
15
+ }
16
+ export default class SubprocessSeam extends Service {
17
+ constructor(ctx: Context);
18
+ /** 把命令名解析成可执行路径(`which`)。找不到返回 `undefined`。 */
19
+ resolveExecutable(cmd: string): Promise<string | undefined>;
20
+ /**
21
+ * 跑一个命令,收全输出。
22
+ *
23
+ * 🔴 **取消是两段的**:先 SIGTERM,`graceMs` 之后才 SIGKILL。
24
+ * 直接 SIGKILL 会让子进程来不及收拾(临时文件、锁),
25
+ * 而那些残留没人记得是谁留下的。
26
+ */
27
+ spawn(spec: SpawnSpec): Promise<SpawnResult>;
28
+ }
29
+ declare module '@deepseek-ai/cordis' {
30
+ interface Context {
31
+ 'sps.subprocess': SubprocessSeam;
32
+ }
33
+ }
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seams/subprocess/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,eAAO,MAAM,UAAU,mBAAmB,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,OAAO;gBACrC,GAAG,EAAE,OAAO;IAIxB,+CAA+C;IACzC,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQjE;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAmC7C;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,gBAAgB,EAAE,cAAc,CAAC;KAClC;CACF"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @module seams/subprocess
3
+ * @description **子进程 seam** —— 插件起外部命令的口子。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 🔴 **不迁移我们自己的 47 个文件。** 它们直用 `node:child_process`,
8
+ * 照旧能跑;这个 seam 是给**插件**用的门。
9
+ * "提供服务" ≠ "把宿主代码搬过来" —— 混为一谈会把一个低成本的口子
10
+ * 估成一次全仓重构。
11
+ *
12
+ * 形状按 dsh(`ctx.subprocess`):`resolveExecutable / spawn`。
13
+ * ⚠️ `spawnTerminal`(分配 PTY)**不做** —— 我们没有终端复用那套,
14
+ * 给一个假的比没有更坏。
15
+ */
16
+ import { execFile, spawn as nodeSpawn } from 'node:child_process';
17
+ import { Service } from '@deepseek-ai/cordis';
18
+ export const SUBPROCESS = 'sps.subprocess';
19
+ export default class SubprocessSeam extends Service {
20
+ constructor(ctx) {
21
+ super(ctx, SUBPROCESS);
22
+ }
23
+ /** 把命令名解析成可执行路径(`which`)。找不到返回 `undefined`。 */
24
+ async resolveExecutable(cmd) {
25
+ return await new Promise((res) => {
26
+ execFile('which', [cmd], (err, stdout) => {
27
+ res(err ? undefined : stdout.trim() || undefined);
28
+ });
29
+ });
30
+ }
31
+ /**
32
+ * 跑一个命令,收全输出。
33
+ *
34
+ * 🔴 **取消是两段的**:先 SIGTERM,`graceMs` 之后才 SIGKILL。
35
+ * 直接 SIGKILL 会让子进程来不及收拾(临时文件、锁),
36
+ * 而那些残留没人记得是谁留下的。
37
+ */
38
+ spawn(spec) {
39
+ const [cmd, ...args] = spec.argv;
40
+ if (!cmd)
41
+ return Promise.reject(new Error('spawn: argv 为空'));
42
+ return new Promise((resolve, reject) => {
43
+ const child = nodeSpawn(cmd, args, {
44
+ cwd: spec.cwd,
45
+ ...(spec.env ? { env: spec.env } : {}),
46
+ });
47
+ let stdout = '';
48
+ let stderr = '';
49
+ let killer;
50
+ child.stdout?.on('data', (d) => {
51
+ stdout += String(d);
52
+ });
53
+ child.stderr?.on('data', (d) => {
54
+ stderr += String(d);
55
+ });
56
+ const onAbort = () => {
57
+ child.kill('SIGTERM');
58
+ killer = setTimeout(() => child.kill('SIGKILL'), spec.graceMs ?? 5000);
59
+ // 别让这个定时器把进程钉住 —— 命令已经在退了
60
+ killer.unref?.();
61
+ };
62
+ spec.signal?.addEventListener('abort', onAbort, { once: true });
63
+ child.on('error', (e) => {
64
+ if (killer)
65
+ clearTimeout(killer);
66
+ reject(e);
67
+ });
68
+ child.on('close', (code) => {
69
+ if (killer)
70
+ clearTimeout(killer);
71
+ spec.signal?.removeEventListener('abort', onAbort);
72
+ resolve({ code, stdout, stderr });
73
+ });
74
+ });
75
+ }
76
+ }
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seams/subprocess/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module seams/tools/jsonSchemaToZod
3
+ * @description JSON Schema → Zod raw shape,给 MCP 的 `registerTool` 用。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 🔴 **为什么需要它**:`sps.tools` 存的是 JSON Schema(插件世界的通用形状,
8
+ * dsh 的 `defineTool` 也规范成这个),而 MCP SDK 的 `registerTool` 只收
9
+ * **Zod raw shape** —— 直接把 JSON Schema 递过去会抛
10
+ * `inputSchema must be a Zod schema or raw shape`。
11
+ *
12
+ * ⚠️ 这条从 0.67 加 `sps.tools` 起就一直断着,而且**没有任何测试红过** ——
13
+ * 因为在这之前注册表里从来没有过真插件的工具(内建工具走的是另一条路,
14
+ * 它们本来就写 `z.object({...})`)。是"真装一个 dsh 插件"才把它撞出来的。
15
+ *
16
+ * 支持的子集按**真插件用到的**来(dsh-map-tools 等):
17
+ * string / number / integer / boolean / array / object / enum,加 required 与 description。
18
+ * 认不出的类型落到 `z.unknown()` —— **不抛**:一个字段描述不了不该让整个工具用不了。
19
+ */
20
+ import { z } from 'zod';
21
+ /** 顶层入口:JSON Schema 的 object → Zod raw shape。 */
22
+ export declare function jsonSchemaToZodShape(schema: unknown): Record<string, z.ZodTypeAny>;
23
+ //# sourceMappingURL=jsonSchemaToZod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonSchemaToZod.d.ts","sourceRoot":"","sources":["../../../src/seams/tools/jsonSchemaToZod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAoDxB,iDAAiD;AACjD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAGlF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @module seams/tools/jsonSchemaToZod
3
+ * @description JSON Schema → Zod raw shape,给 MCP 的 `registerTool` 用。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 🔴 **为什么需要它**:`sps.tools` 存的是 JSON Schema(插件世界的通用形状,
8
+ * dsh 的 `defineTool` 也规范成这个),而 MCP SDK 的 `registerTool` 只收
9
+ * **Zod raw shape** —— 直接把 JSON Schema 递过去会抛
10
+ * `inputSchema must be a Zod schema or raw shape`。
11
+ *
12
+ * ⚠️ 这条从 0.67 加 `sps.tools` 起就一直断着,而且**没有任何测试红过** ——
13
+ * 因为在这之前注册表里从来没有过真插件的工具(内建工具走的是另一条路,
14
+ * 它们本来就写 `z.object({...})`)。是"真装一个 dsh 插件"才把它撞出来的。
15
+ *
16
+ * 支持的子集按**真插件用到的**来(dsh-map-tools 等):
17
+ * string / number / integer / boolean / array / object / enum,加 required 与 description。
18
+ * 认不出的类型落到 `z.unknown()` —— **不抛**:一个字段描述不了不该让整个工具用不了。
19
+ */
20
+ import { z } from 'zod';
21
+ function nodeToZod(node) {
22
+ if (Array.isArray(node.enum) && node.enum.length > 0) {
23
+ const vals = node.enum.filter((v) => typeof v === 'string');
24
+ // 非字符串枚举(数字等)交给下面按 type 处理 —— z.enum 只吃字符串
25
+ if (vals.length === node.enum.length) {
26
+ return z.enum(vals);
27
+ }
28
+ }
29
+ switch (node.type) {
30
+ case 'string':
31
+ return z.string();
32
+ case 'number':
33
+ case 'integer':
34
+ return z.number();
35
+ case 'boolean':
36
+ return z.boolean();
37
+ case 'array':
38
+ return z.array(node.items ? nodeToZod(node.items) : z.unknown());
39
+ case 'object':
40
+ return z.object(shapeOf(node));
41
+ default:
42
+ return z.unknown();
43
+ }
44
+ }
45
+ function shapeOf(schema) {
46
+ const props = schema.properties ?? {};
47
+ // 🔴 `required` 有两种写法:标准 JSON Schema 是**数组**,
48
+ // 但也见过字段级的 `required: true`(dsh 的 defineTool 产出里就有)。
49
+ // 只认数组的话,那类工具的必填会全部变成可选 —— 不报错,但模型会漏传参。
50
+ const requiredList = new Set(Array.isArray(schema.required) ? schema.required : []);
51
+ const out = {};
52
+ for (const [key, raw] of Object.entries(props)) {
53
+ let t = nodeToZod(raw);
54
+ if (raw.description)
55
+ t = t.describe(raw.description);
56
+ const isRequired = requiredList.has(key) || raw.required === true;
57
+ out[key] = isRequired ? t : t.optional();
58
+ }
59
+ return out;
60
+ }
61
+ /** 顶层入口:JSON Schema 的 object → Zod raw shape。 */
62
+ export function jsonSchemaToZodShape(schema) {
63
+ if (!schema || typeof schema !== 'object')
64
+ return {};
65
+ return shapeOf(schema);
66
+ }
67
+ //# sourceMappingURL=jsonSchemaToZod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonSchemaToZod.js","sourceRoot":"","sources":["../../../src/seams/tools/jsonSchemaToZod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,SAAS,SAAS,CAAC,IAAoB;IACrC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QACzE,2CAA2C;QAC3C,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACrC,OAAO,CAAC,CAAC,IAAI,CAAC,IAA6B,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IACD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;QACpB,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;QACpB,KAAK,SAAS;YACZ,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;QACrB,KAAK,OAAO;YACV,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACnE,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACjC;YACE,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,MAAsB;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;IACtC,8CAA8C;IAC9C,wDAAwD;IACxD,2CAA2C;IAC3C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpF,MAAM,GAAG,GAAiC,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,IAAI,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,GAAG,CAAC,WAAW;YAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACrD,MAAM,UAAU,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC;QAClE,GAAG,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,oBAAoB,CAAC,MAAe;IAClD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACrD,OAAO,OAAO,CAAC,MAAwB,CAAC,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @module seams/webserver
3
+ * @description **HTTP 路由 seam** —— 让插件往 console 的 HTTP 面注册自己的路由。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 类型:**甲 · 工厂型**(进程里一个)。dsh 抽样里第二高频(22/77)。
8
+ *
9
+ * ⚠️ **方向别搞混**:插件自己 `fetch` 外部服务(出方向)不需要任何服务,
10
+ * 也拦不住 —— 插件本来就是进程内受信任代码。这里管的是**入方向**:
11
+ * 插件在**我们 console 的鉴权面**上开一条路由,那条路由天然带 `sps_token` cookie。
12
+ *
13
+ * 🔴 **护栏形状(Eddy 2026-08-21 拍板)——防意外,不防故意:**
14
+ * ```
15
+ * 默认 路由自动落 /plugins/<插件id>/* 零配置零摩擦
16
+ * 接管宿主 显式声明 takeover: true 允许(sps-cli 是用户自己的工具)
17
+ * 可见 listTakeovers() 供插件页展示 ← 关键在这一格
18
+ * ```
19
+ * 真正的失败模式不是恶意,是**遗忘**:半年后某个接口行为不对,
20
+ * 没人记得有插件接管了它,排查会一路查进宿主代码 —— 而宿主代码是对的。
21
+ * **能接管 ≠ 接管了却没人知道。**
22
+ *
23
+ * 🔴 **为什么是"可变路由表 + 一条 catch-all",而不是往 Hono 上加路由:**
24
+ * Hono 注册后**删不掉**。而插件要能热卸载(停用/卸载当场下线)——
25
+ * 往 Hono 上加的话,停用之后那条路由还在,只是处理函数变成了幽灵。
26
+ */
27
+ import { type Context, Service } from '@deepseek-ai/cordis';
28
+ export declare const WEBSERVER = "sps.webserver";
29
+ export type RouteKind = 'exact' | 'prefix';
30
+ export interface WebRoute {
31
+ kind: RouteKind;
32
+ /** 相对路径。默认会被前缀成 `/plugins/<owner>/<path>`;`takeover` 时按原样。 */
33
+ path: string;
34
+ /**
35
+ * 原生 Node 处理函数 —— **与 dsh 的 `WebRoute.handler` 同形**,
36
+ * 这样 dsh 插件不用改。
37
+ */
38
+ handler: (req: unknown, res: unknown) => void | Promise<void>;
39
+ /**
40
+ * 接管宿主已有路径。**允许,但必须显式写出来**,而且会出现在插件页上。
41
+ */
42
+ takeover?: boolean;
43
+ }
44
+ interface Entry extends WebRoute {
45
+ owner: string;
46
+ /** 真正生效的绝对路径(前缀过的,或 takeover 的原样)。 */
47
+ full: string;
48
+ }
49
+ export declare class RouteConflictError extends Error {
50
+ }
51
+ /** 插件路由的默认前缀。 */
52
+ export declare function pluginPrefix(owner: string): string;
53
+ export default class WebServerSeam extends Service {
54
+ /** full path → entry。**可变表**,热卸载靠删这里的项。 */
55
+ private readonly routes;
56
+ constructor(ctx: Context);
57
+ /**
58
+ * 注册一条路由。`owner` = 插件 id,决定默认前缀,也是"谁接管了什么"的归属。
59
+ *
60
+ * 🔴 **重复路径抛冲突,不是后注册的静默赢** ——
61
+ * 静默赢会让两个插件里"没生效的那个"完全无法解释。
62
+ */
63
+ register(owner: string, route: WebRoute): () => void;
64
+ /**
65
+ * 找一条能处理该路径的路由。exact 优先于 prefix;prefix 里**最长的赢**
66
+ * (否则 `/a` 会抢走 `/a/b` 的请求,而后者显然更具体)。
67
+ */
68
+ match(path: string): Entry | undefined;
69
+ /**
70
+ * 谁接管了哪些宿主路径 —— **插件页要摆出来的就是它**。
71
+ * 这是整套护栏里最关键的一格:能接管不可怕,接管了没人知道才可怕。
72
+ */
73
+ listTakeovers(): {
74
+ owner: string;
75
+ path: string;
76
+ kind: RouteKind;
77
+ }[];
78
+ /** 全部路由(诊断用)。 */
79
+ list(): {
80
+ owner: string;
81
+ path: string;
82
+ kind: RouteKind;
83
+ takeover: boolean;
84
+ }[];
85
+ }
86
+ declare module '@deepseek-ai/cordis' {
87
+ interface Context {
88
+ 'sps.webserver': WebServerSeam;
89
+ }
90
+ }
91
+ export {};
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seams/webserver/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,eAAO,MAAM,SAAS,kBAAkB,CAAC;AAEzC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,KAAM,SAAQ,QAAQ;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAEhD,iBAAiB;AACjB,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,OAAO;IAChD,2CAA2C;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,GAAG,EAAE,OAAO;IAIxB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,IAAI;IAiBpD;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAatC;;;OAGG;IACH,aAAa,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,EAAE;IAMnE,iBAAiB;IACjB,IAAI,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE;CAQ9E;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,eAAe,EAAE,aAAa,CAAC;KAChC;CACF"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @module seams/webserver
3
+ * @description **HTTP 路由 seam** —— 让插件往 console 的 HTTP 面注册自己的路由。
4
+ *
5
+ * @layer seam
6
+ *
7
+ * 类型:**甲 · 工厂型**(进程里一个)。dsh 抽样里第二高频(22/77)。
8
+ *
9
+ * ⚠️ **方向别搞混**:插件自己 `fetch` 外部服务(出方向)不需要任何服务,
10
+ * 也拦不住 —— 插件本来就是进程内受信任代码。这里管的是**入方向**:
11
+ * 插件在**我们 console 的鉴权面**上开一条路由,那条路由天然带 `sps_token` cookie。
12
+ *
13
+ * 🔴 **护栏形状(Eddy 2026-08-21 拍板)——防意外,不防故意:**
14
+ * ```
15
+ * 默认 路由自动落 /plugins/<插件id>/* 零配置零摩擦
16
+ * 接管宿主 显式声明 takeover: true 允许(sps-cli 是用户自己的工具)
17
+ * 可见 listTakeovers() 供插件页展示 ← 关键在这一格
18
+ * ```
19
+ * 真正的失败模式不是恶意,是**遗忘**:半年后某个接口行为不对,
20
+ * 没人记得有插件接管了它,排查会一路查进宿主代码 —— 而宿主代码是对的。
21
+ * **能接管 ≠ 接管了却没人知道。**
22
+ *
23
+ * 🔴 **为什么是"可变路由表 + 一条 catch-all",而不是往 Hono 上加路由:**
24
+ * Hono 注册后**删不掉**。而插件要能热卸载(停用/卸载当场下线)——
25
+ * 往 Hono 上加的话,停用之后那条路由还在,只是处理函数变成了幽灵。
26
+ */
27
+ import { Service } from '@deepseek-ai/cordis';
28
+ export const WEBSERVER = 'sps.webserver';
29
+ export class RouteConflictError extends Error {
30
+ }
31
+ /** 插件路由的默认前缀。 */
32
+ export function pluginPrefix(owner) {
33
+ return `/plugins/${owner}`;
34
+ }
35
+ export default class WebServerSeam extends Service {
36
+ /** full path → entry。**可变表**,热卸载靠删这里的项。 */
37
+ routes = new Map();
38
+ constructor(ctx) {
39
+ super(ctx, WEBSERVER);
40
+ }
41
+ /**
42
+ * 注册一条路由。`owner` = 插件 id,决定默认前缀,也是"谁接管了什么"的归属。
43
+ *
44
+ * 🔴 **重复路径抛冲突,不是后注册的静默赢** ——
45
+ * 静默赢会让两个插件里"没生效的那个"完全无法解释。
46
+ */
47
+ register(owner, route) {
48
+ const rel = route.path.startsWith('/') ? route.path : `/${route.path}`;
49
+ const full = route.takeover ? rel : `${pluginPrefix(owner)}${rel}`;
50
+ const key = `${route.kind}:${full}`;
51
+ const existing = this.routes.get(key);
52
+ if (existing) {
53
+ throw new RouteConflictError(`路由 ${full} 已被插件 ${existing.owner} 占用(${route.kind});同一路径不能注册两次`);
54
+ }
55
+ this.routes.set(key, { ...route, owner, full });
56
+ return () => {
57
+ const cur = this.routes.get(key);
58
+ if (cur && cur.owner === owner)
59
+ this.routes.delete(key);
60
+ };
61
+ }
62
+ /**
63
+ * 找一条能处理该路径的路由。exact 优先于 prefix;prefix 里**最长的赢**
64
+ * (否则 `/a` 会抢走 `/a/b` 的请求,而后者显然更具体)。
65
+ */
66
+ match(path) {
67
+ const exact = this.routes.get(`exact:${path}`);
68
+ if (exact)
69
+ return exact;
70
+ let best;
71
+ for (const e of this.routes.values()) {
72
+ if (e.kind !== 'prefix')
73
+ continue;
74
+ if (path === e.full || path.startsWith(`${e.full}/`)) {
75
+ if (!best || e.full.length > best.full.length)
76
+ best = e;
77
+ }
78
+ }
79
+ return best;
80
+ }
81
+ /**
82
+ * 谁接管了哪些宿主路径 —— **插件页要摆出来的就是它**。
83
+ * 这是整套护栏里最关键的一格:能接管不可怕,接管了没人知道才可怕。
84
+ */
85
+ listTakeovers() {
86
+ return [...this.routes.values()]
87
+ .filter((e) => e.takeover)
88
+ .map((e) => ({ owner: e.owner, path: e.full, kind: e.kind }));
89
+ }
90
+ /** 全部路由(诊断用)。 */
91
+ list() {
92
+ return [...this.routes.values()].map((e) => ({
93
+ owner: e.owner,
94
+ path: e.full,
95
+ kind: e.kind,
96
+ takeover: e.takeover === true,
97
+ }));
98
+ }
99
+ }
100
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seams/webserver/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAgB,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,CAAC,MAAM,SAAS,GAAG,eAAe,CAAC;AAyBzC,MAAM,OAAO,kBAAmB,SAAQ,KAAK;CAAG;AAEhD,iBAAiB;AACjB,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,YAAY,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,OAAO;IAChD,2CAA2C;IAC1B,MAAM,GAAG,IAAI,GAAG,EAAiB,CAAC;IAEnD,YAAY,GAAY;QACtB,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,KAAa,EAAE,KAAe;QACrC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACvE,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;QACnE,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,IAAI,kBAAkB,CAC1B,MAAM,IAAI,SAAS,QAAQ,CAAC,KAAK,OAAO,KAAK,CAAC,IAAI,cAAc,CACjE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE;YACV,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1D,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAY;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC/C,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;QACxB,IAAI,IAAuB,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;gBAAE,SAAS;YAClC,IAAI,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACrD,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM;oBAAE,IAAI,GAAG,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,aAAa;QACX,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;aAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,iBAAiB;IACjB,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3C,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,IAAI;SAC9B,CAAC,CAAC,CAAC;IACN,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralai/sps-cli",
3
- "version": "0.68.0",
3
+ "version": "0.69.1",
4
4
  "description": "SPS CLI — AI-driven development pipeline orchestrator",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",