@love-moon/ai-sdk 0.3.1 → 0.4.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 (59) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/client.d.ts +15 -0
  5. package/dist/client.js +103 -1
  6. package/dist/external-provider-registry.js +4 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +3 -0
  9. package/dist/manager/account.d.ts +6 -0
  10. package/dist/manager/account.js +121 -0
  11. package/dist/manager/auth-parser.d.ts +27 -0
  12. package/dist/manager/auth-parser.js +54 -0
  13. package/dist/manager/config.d.ts +6 -0
  14. package/dist/manager/config.js +32 -0
  15. package/dist/manager/index.d.ts +12 -0
  16. package/dist/manager/index.js +11 -0
  17. package/dist/manager/install.d.ts +9 -0
  18. package/dist/manager/install.js +117 -0
  19. package/dist/manager/manager.d.ts +51 -0
  20. package/dist/manager/manager.js +105 -0
  21. package/dist/manager/network.d.ts +8 -0
  22. package/dist/manager/network.js +46 -0
  23. package/dist/manager/paths.d.ts +6 -0
  24. package/dist/manager/paths.js +16 -0
  25. package/dist/manager/quota/cache.d.ts +9 -0
  26. package/dist/manager/quota/cache.js +33 -0
  27. package/dist/manager/quota/claude.d.ts +19 -0
  28. package/dist/manager/quota/claude.js +193 -0
  29. package/dist/manager/quota/codex.d.ts +27 -0
  30. package/dist/manager/quota/codex.js +182 -0
  31. package/dist/manager/quota/copilot.d.ts +64 -0
  32. package/dist/manager/quota/copilot.js +718 -0
  33. package/dist/manager/quota/external.d.ts +29 -0
  34. package/dist/manager/quota/external.js +176 -0
  35. package/dist/manager/quota/headers.d.ts +5 -0
  36. package/dist/manager/quota/headers.js +29 -0
  37. package/dist/manager/quota/kimi.d.ts +24 -0
  38. package/dist/manager/quota/kimi.js +230 -0
  39. package/dist/manager/types.d.ts +166 -0
  40. package/dist/manager/types.js +1 -0
  41. package/dist/providers/chat-web-session.d.ts +218 -0
  42. package/dist/providers/chat-web-session.js +584 -0
  43. package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
  44. package/dist/providers/claude-agent-sdk-session.js +109 -1
  45. package/dist/providers/codex-app-server-session.d.ts +107 -0
  46. package/dist/providers/codex-app-server-session.js +479 -9
  47. package/dist/providers/copilot-sdk-session.d.ts +9 -1
  48. package/dist/providers/copilot-sdk-session.js +48 -0
  49. package/dist/resume/chat-web.d.ts +20 -0
  50. package/dist/resume/chat-web.js +44 -0
  51. package/dist/resume/index.js +2 -0
  52. package/dist/session-factory.d.ts +3 -1
  53. package/dist/session-factory.js +17 -4
  54. package/dist/shared.d.ts +159 -0
  55. package/dist/shared.js +111 -0
  56. package/dist/transports/codex-app-server-transport.d.ts +1 -0
  57. package/dist/transports/codex-app-server-transport.js +45 -1
  58. package/dist/worker.js +19 -5
  59. package/package.json +10 -3
@@ -0,0 +1,20 @@
1
+ /**
2
+ * chat-web's "session" is a Chromium browser context, not a conversation id.
3
+ * There is no native resume primitive: closing and reopening produces a
4
+ * fresh ChatSession with a new in-memory chat. We satisfy ai-sdk's resume
5
+ * contract by surfacing a synthetic session id and a "best effort" context,
6
+ * but downstream callers should treat resume here as a no-op.
7
+ */
8
+ export function buildCliArgs(sessionId: any): string[];
9
+ export function findSessionPath(): Promise<null>;
10
+ export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
11
+ provider: any;
12
+ sessionId: any;
13
+ sessionPath: null;
14
+ cwd: any;
15
+ debugMetadata: {
16
+ cwdSource: any;
17
+ sessionPath: null;
18
+ };
19
+ }>;
20
+ export const BACKEND: "chat-web";
@@ -0,0 +1,44 @@
1
+ import { buildResumeContext, normalizeSessionId } from "./shared.js";
2
+ export const BACKEND = "chat-web";
3
+ /**
4
+ * chat-web's "session" is a Chromium browser context, not a conversation id.
5
+ * There is no native resume primitive: closing and reopening produces a
6
+ * fresh ChatSession with a new in-memory chat. We satisfy ai-sdk's resume
7
+ * contract by surfacing a synthetic session id and a "best effort" context,
8
+ * but downstream callers should treat resume here as a no-op.
9
+ */
10
+ export function buildCliArgs(sessionId) {
11
+ // chat-web has no CLI to pass these args to (it's an in-process SDK), but
12
+ // the resume contract requires every backend to return a non-empty arg
13
+ // vector so downstream resume dispatch stays uniform. We emit a stable,
14
+ // recognisable token; consumers should ignore it for chat-web.
15
+ const normalizedSessionId = normalizeSessionId(sessionId);
16
+ if (!normalizedSessionId) {
17
+ return [];
18
+ }
19
+ return [`--resume-session-id=${normalizedSessionId}`];
20
+ }
21
+ export async function findSessionPath() {
22
+ // No on-disk JSONL session file — the persistent state lives in the
23
+ // browser profile dir, but that's not what callers expect from this hook.
24
+ return null;
25
+ }
26
+ export async function resolveResumeContext(sessionId, options = {}) {
27
+ const normalizedSessionId = normalizeSessionId(sessionId);
28
+ if (!normalizedSessionId) {
29
+ throw new Error("--resume requires a session id");
30
+ }
31
+ const cwd = typeof options?.cwd === "string" && options.cwd.trim()
32
+ ? options.cwd.trim()
33
+ : process.cwd();
34
+ return buildResumeContext({
35
+ provider: "chat-web",
36
+ sessionId: normalizedSessionId,
37
+ sessionPath: null,
38
+ cwd,
39
+ cwdSource: "process_cwd",
40
+ extraDebug: {
41
+ note: "chat-web does not support cross-process conversation resume; a fresh ChatSession will be opened.",
42
+ },
43
+ });
44
+ }
@@ -1,5 +1,6 @@
1
1
  import { BUILT_IN_BACKENDS, getBuiltInBackendEntry, normalizeBuiltInBackend, } from "../built-in-backends.js";
2
2
  import { getExternalProviderDescriptor, resolveExternalBackend, } from "../external-provider-registry.js";
3
+ import * as chatWeb from "./chat-web.js";
3
4
  import * as claude from "./claude.js";
4
5
  import * as codex from "./codex.js";
5
6
  import * as copilot from "./copilot.js";
@@ -25,6 +26,7 @@ const RESUME_MODULES_BY_BACKEND = new Map([
25
26
  ["copilot", copilot],
26
27
  ["kimi", kimi],
27
28
  ["opencode", opencode],
29
+ ["chat-web", chatWeb],
28
30
  ]);
29
31
  // Sanity-check at module load:
30
32
  // (1) Every registered resume module must point at a real built-in backend.
@@ -10,7 +10,9 @@ export const COPILOT_PROVIDER_VARIANT: "copilot-sdk";
10
10
  export const KIMI_PROVIDER_VARIANT: "kimi-cli-wire";
11
11
  export const KIMI_PRINT_PROVIDER_VARIANT: "kimi-cli-print";
12
12
  export const OPENCODE_PROVIDER_VARIANT: "opencode-sdk";
13
+ export const CHAT_WEB_PROVIDER_VARIANT: "chat-web-session";
13
14
  import { BUILT_IN_BACKENDS } from "./built-in-backends.js";
15
+ import { ChatWebSession } from "./providers/chat-web-session.js";
14
16
  import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
15
17
  import { CodexExecSession } from "./providers/codex-exec-session.js";
16
18
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
@@ -18,4 +20,4 @@ import { CopilotSdkSession } from "./providers/copilot-sdk-session.js";
18
20
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
19
21
  import { KimiPrintSession } from "./providers/kimi-print-session.js";
20
22
  import { OpencodeSdkSession } from "./providers/opencode-sdk-session.js";
21
- export { BUILT_IN_BACKENDS, CodexAppServerSession, CodexExecSession, ClaudeAgentSdkSession, CopilotSdkSession, KimiCliSession, KimiPrintSession, OpencodeSdkSession };
23
+ export { BUILT_IN_BACKENDS, ChatWebSession, CodexAppServerSession, CodexExecSession, ClaudeAgentSdkSession, CopilotSdkSession, KimiCliSession, KimiPrintSession, OpencodeSdkSession };
@@ -1,4 +1,5 @@
1
- import { BUILT_IN_BACKENDS, CLAUDE_AGENT_SDK_VARIANT, CODEX_APP_SERVER_VARIANT, CODEX_EXEC_VARIANT, COPILOT_SDK_VARIANT, KIMI_CLI_PRINT_VARIANT, KIMI_CLI_WIRE_VARIANT, OPENCODE_SDK_VARIANT, getBuiltInBackendEntry, isBuiltInBackend, listBuiltInBackends, normalizeBuiltInBackend, } from "./built-in-backends.js";
1
+ import { BUILT_IN_BACKENDS, CHAT_WEB_SESSION_VARIANT, CLAUDE_AGENT_SDK_VARIANT, CODEX_APP_SERVER_VARIANT, CODEX_EXEC_VARIANT, COPILOT_SDK_VARIANT, KIMI_CLI_PRINT_VARIANT, KIMI_CLI_WIRE_VARIANT, OPENCODE_SDK_VARIANT, getBuiltInBackendEntry, isBuiltInBackend, listBuiltInBackends, normalizeBuiltInBackend, } from "./built-in-backends.js";
2
+ import { ChatWebSession } from "./providers/chat-web-session.js";
2
3
  import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
3
4
  import { CodexExecSession } from "./providers/codex-exec-session.js";
4
5
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
@@ -14,12 +15,22 @@ export const COPILOT_PROVIDER_VARIANT = COPILOT_SDK_VARIANT;
14
15
  export const KIMI_PROVIDER_VARIANT = KIMI_CLI_WIRE_VARIANT;
15
16
  export const KIMI_PRINT_PROVIDER_VARIANT = KIMI_CLI_PRINT_VARIANT;
16
17
  export const OPENCODE_PROVIDER_VARIANT = OPENCODE_SDK_VARIANT;
18
+ export const CHAT_WEB_PROVIDER_VARIANT = CHAT_WEB_SESSION_VARIANT;
17
19
  const SESSION_FACTORIES_BY_BACKEND = new Map([
18
20
  [
19
21
  "codex",
20
- (backend, options) => hasStructuredOutputPreference(options)
21
- ? new CodexExecSession(backend, options)
22
- : new CodexAppServerSession(backend, options),
22
+ (backend, options) => {
23
+ // Goal mode requires the app-server transport (codex-exec has no
24
+ // `thread/goal/set` JSON-RPC). When goalMode is requested we always
25
+ // pick the app-server variant, even if a structured-output preference
26
+ // would otherwise route to codex-exec.
27
+ if (options?.goalMode === true) {
28
+ return new CodexAppServerSession(backend, options);
29
+ }
30
+ return hasStructuredOutputPreference(options)
31
+ ? new CodexExecSession(backend, options)
32
+ : new CodexAppServerSession(backend, options);
33
+ },
23
34
  ],
24
35
  ["claude", (backend, options) => new ClaudeAgentSdkSession(backend, options)],
25
36
  ["copilot", (backend, options) => new CopilotSdkSession(backend, options)],
@@ -30,6 +41,7 @@ const SESSION_FACTORIES_BY_BACKEND = new Map([
30
41
  : new KimiCliSession(backend, options),
31
42
  ],
32
43
  ["opencode", (backend, options) => new OpencodeSdkSession(backend, options)],
44
+ ["chat-web", (backend, options) => new ChatWebSession(backend, options)],
33
45
  ]);
34
46
  function hasStructuredOutputPreference(options = {}) {
35
47
  if (!options || typeof options !== "object") {
@@ -106,6 +118,7 @@ export async function createLocalAiSession(backend, options = {}) {
106
118
  return await descriptor.createSession(normalized, options);
107
119
  }
108
120
  export { BUILT_IN_BACKENDS };
121
+ export { ChatWebSession };
109
122
  export { CodexAppServerSession };
110
123
  export { CodexExecSession };
111
124
  export { ClaudeAgentSdkSession };
package/dist/shared.d.ts CHANGED
@@ -1,3 +1,21 @@
1
+ /**
2
+ * @param {unknown} value
3
+ * @returns {value is GoalStatus}
4
+ */
5
+ export function isGoalStatus(value: unknown): value is GoalStatus;
6
+ /**
7
+ * @param {unknown} value
8
+ * @returns {boolean}
9
+ */
10
+ export function isTerminalGoalStatus(value: unknown): boolean;
11
+ /**
12
+ * Resolve a session's capability snapshot, falling back to
13
+ * {@link DEFAULT_SESSION_CAPABILITIES}.
14
+ *
15
+ * @param {unknown} session
16
+ * @returns {SessionCapabilities}
17
+ */
18
+ export function resolveSessionCapabilities(session: unknown): SessionCapabilities;
1
19
  export function normalizeLogger(logger: any): any;
2
20
  export function emitLog(logger: any, message: any): void;
3
21
  export function truncateText(value: any, maxLen?: number): string;
@@ -16,3 +34,144 @@ export function proxyToEnv(envConfig: any): {};
16
34
  export function withoutCopilotGithubTokenEnv(env: any): any;
17
35
  export function serializeError(error: any): any;
18
36
  export function reviveError(payload: any): Error;
37
+ /**
38
+ * Goal-mode capability typedefs shared by AI SDK providers that implement
39
+ * the optional {@link runGoal} method.
40
+ *
41
+ * Callers should treat goal support as opt-in: detect with
42
+ * `typeof session.runGoal === "function"`. When unsupported, do NOT silently
43
+ * fall back to {@link runTurn} — surface a clear error instead.
44
+ *
45
+ * Session creation may also accept `{ goalMode: true }` so the underlying
46
+ * transport can adjust its spawn arguments (e.g. Codex app-server requires
47
+ * `--enable goals` at boot time; dynamic enablement is not supported).
48
+ *
49
+ * @typedef {"active"|"paused"|"blocked"|"usageLimited"|"budgetLimited"|"complete"} GoalStatus
50
+ *
51
+ * @typedef {Object} GoalRequest
52
+ * @property {string} objective
53
+ * @property {number|null} [tokenBudget]
54
+ * @property {{ type: "issue"|"manual", issueId?: string, taskId?: string }} [source]
55
+ *
56
+ * @typedef {Object} GoalState
57
+ * @property {string} [id]
58
+ * @property {string} [threadId]
59
+ * @property {string} objective
60
+ * @property {GoalStatus} status
61
+ * @property {number|null} [tokenBudget]
62
+ *
63
+ * @typedef {Object} GoalResult
64
+ * @property {string} text
65
+ * @property {GoalState} goal
66
+ * @property {unknown} [usage]
67
+ * @property {Record<string, unknown>} [metadata]
68
+ */
69
+ /**
70
+ * Runtime list of valid {@link GoalStatus} values. Useful for validation.
71
+ * Terminal statuses are everything except "active" and "paused".
72
+ * @type {ReadonlyArray<GoalStatus>}
73
+ */
74
+ export const GOAL_STATUSES: ReadonlyArray<GoalStatus>;
75
+ /**
76
+ * Terminal goal statuses (the goal will not produce further updates).
77
+ * @type {ReadonlyArray<GoalStatus>}
78
+ */
79
+ export const TERMINAL_GOAL_STATUSES: ReadonlyArray<GoalStatus>;
80
+ /**
81
+ * Session capability flags advertised in {@link SessionSnapshot.capabilities}.
82
+ *
83
+ * Providers may declare optional features (e.g. `goal`) so that callers can
84
+ * detect support without reflecting on method names (which is unreliable when
85
+ * a wrapping proxy unconditionally forwards every method).
86
+ *
87
+ * @typedef {Object} SessionCapabilities
88
+ * @property {boolean} [goal]
89
+ */
90
+ /**
91
+ * Default capabilities assigned to providers that do not opt into any optional
92
+ * feature. Treat the returned object as read-only; clone before mutating.
93
+ *
94
+ * @type {Readonly<SessionCapabilities>}
95
+ */
96
+ export const DEFAULT_SESSION_CAPABILITIES: Readonly<SessionCapabilities>;
97
+ /**
98
+ * Goal-mode capability typedefs shared by AI SDK providers that implement
99
+ * the optional {@link runGoal} method.
100
+ *
101
+ * Callers should treat goal support as opt-in: detect with
102
+ * `typeof session.runGoal === "function"`. When unsupported, do NOT silently
103
+ * fall back to {@link runTurn} — surface a clear error instead.
104
+ *
105
+ * Session creation may also accept `{ goalMode: true }` so the underlying
106
+ * transport can adjust its spawn arguments (e.g. Codex app-server requires
107
+ * `--enable goals` at boot time; dynamic enablement is not supported).
108
+ */
109
+ export type GoalStatus = "active" | "paused" | "blocked" | "usageLimited" | "budgetLimited" | "complete";
110
+ /**
111
+ * Goal-mode capability typedefs shared by AI SDK providers that implement
112
+ * the optional {@link runGoal} method.
113
+ *
114
+ * Callers should treat goal support as opt-in: detect with
115
+ * `typeof session.runGoal === "function"`. When unsupported, do NOT silently
116
+ * fall back to {@link runTurn} — surface a clear error instead.
117
+ *
118
+ * Session creation may also accept `{ goalMode: true }` so the underlying
119
+ * transport can adjust its spawn arguments (e.g. Codex app-server requires
120
+ * `--enable goals` at boot time; dynamic enablement is not supported).
121
+ */
122
+ export type GoalRequest = {
123
+ objective: string;
124
+ tokenBudget?: number | null | undefined;
125
+ source?: {
126
+ type: "issue" | "manual";
127
+ issueId?: string;
128
+ taskId?: string;
129
+ } | undefined;
130
+ };
131
+ /**
132
+ * Goal-mode capability typedefs shared by AI SDK providers that implement
133
+ * the optional {@link runGoal} method.
134
+ *
135
+ * Callers should treat goal support as opt-in: detect with
136
+ * `typeof session.runGoal === "function"`. When unsupported, do NOT silently
137
+ * fall back to {@link runTurn} — surface a clear error instead.
138
+ *
139
+ * Session creation may also accept `{ goalMode: true }` so the underlying
140
+ * transport can adjust its spawn arguments (e.g. Codex app-server requires
141
+ * `--enable goals` at boot time; dynamic enablement is not supported).
142
+ */
143
+ export type GoalState = {
144
+ id?: string | undefined;
145
+ threadId?: string | undefined;
146
+ objective: string;
147
+ status: GoalStatus;
148
+ tokenBudget?: number | null | undefined;
149
+ };
150
+ /**
151
+ * Goal-mode capability typedefs shared by AI SDK providers that implement
152
+ * the optional {@link runGoal} method.
153
+ *
154
+ * Callers should treat goal support as opt-in: detect with
155
+ * `typeof session.runGoal === "function"`. When unsupported, do NOT silently
156
+ * fall back to {@link runTurn} — surface a clear error instead.
157
+ *
158
+ * Session creation may also accept `{ goalMode: true }` so the underlying
159
+ * transport can adjust its spawn arguments (e.g. Codex app-server requires
160
+ * `--enable goals` at boot time; dynamic enablement is not supported).
161
+ */
162
+ export type GoalResult = {
163
+ text: string;
164
+ goal: GoalState;
165
+ usage?: unknown;
166
+ metadata?: Record<string, unknown> | undefined;
167
+ };
168
+ /**
169
+ * Session capability flags advertised in {@link SessionSnapshot.capabilities}.
170
+ *
171
+ * Providers may declare optional features (e.g. `goal`) so that callers can
172
+ * detect support without reflecting on method names (which is unreliable when
173
+ * a wrapping proxy unconditionally forwards every method).
174
+ */
175
+ export type SessionCapabilities = {
176
+ goal?: boolean | undefined;
177
+ };
package/dist/shared.js CHANGED
@@ -2,6 +2,117 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import yaml from "js-yaml";
5
+ /**
6
+ * Goal-mode capability typedefs shared by AI SDK providers that implement
7
+ * the optional {@link runGoal} method.
8
+ *
9
+ * Callers should treat goal support as opt-in: detect with
10
+ * `typeof session.runGoal === "function"`. When unsupported, do NOT silently
11
+ * fall back to {@link runTurn} — surface a clear error instead.
12
+ *
13
+ * Session creation may also accept `{ goalMode: true }` so the underlying
14
+ * transport can adjust its spawn arguments (e.g. Codex app-server requires
15
+ * `--enable goals` at boot time; dynamic enablement is not supported).
16
+ *
17
+ * @typedef {"active"|"paused"|"blocked"|"usageLimited"|"budgetLimited"|"complete"} GoalStatus
18
+ *
19
+ * @typedef {Object} GoalRequest
20
+ * @property {string} objective
21
+ * @property {number|null} [tokenBudget]
22
+ * @property {{ type: "issue"|"manual", issueId?: string, taskId?: string }} [source]
23
+ *
24
+ * @typedef {Object} GoalState
25
+ * @property {string} [id]
26
+ * @property {string} [threadId]
27
+ * @property {string} objective
28
+ * @property {GoalStatus} status
29
+ * @property {number|null} [tokenBudget]
30
+ *
31
+ * @typedef {Object} GoalResult
32
+ * @property {string} text
33
+ * @property {GoalState} goal
34
+ * @property {unknown} [usage]
35
+ * @property {Record<string, unknown>} [metadata]
36
+ */
37
+ /**
38
+ * Runtime list of valid {@link GoalStatus} values. Useful for validation.
39
+ * Terminal statuses are everything except "active" and "paused".
40
+ * @type {ReadonlyArray<GoalStatus>}
41
+ */
42
+ export const GOAL_STATUSES = Object.freeze([
43
+ "active",
44
+ "paused",
45
+ "blocked",
46
+ "usageLimited",
47
+ "budgetLimited",
48
+ "complete",
49
+ ]);
50
+ /**
51
+ * Terminal goal statuses (the goal will not produce further updates).
52
+ * @type {ReadonlyArray<GoalStatus>}
53
+ */
54
+ export const TERMINAL_GOAL_STATUSES = Object.freeze([
55
+ "blocked",
56
+ "usageLimited",
57
+ "budgetLimited",
58
+ "complete",
59
+ ]);
60
+ /**
61
+ * @param {unknown} value
62
+ * @returns {value is GoalStatus}
63
+ */
64
+ export function isGoalStatus(value) {
65
+ return typeof value === "string" && GOAL_STATUSES.includes(/** @type {GoalStatus} */ (value));
66
+ }
67
+ /**
68
+ * @param {unknown} value
69
+ * @returns {boolean}
70
+ */
71
+ export function isTerminalGoalStatus(value) {
72
+ return typeof value === "string" && TERMINAL_GOAL_STATUSES.includes(/** @type {GoalStatus} */ (value));
73
+ }
74
+ /**
75
+ * Session capability flags advertised in {@link SessionSnapshot.capabilities}.
76
+ *
77
+ * Providers may declare optional features (e.g. `goal`) so that callers can
78
+ * detect support without reflecting on method names (which is unreliable when
79
+ * a wrapping proxy unconditionally forwards every method).
80
+ *
81
+ * @typedef {Object} SessionCapabilities
82
+ * @property {boolean} [goal]
83
+ */
84
+ /**
85
+ * Default capabilities assigned to providers that do not opt into any optional
86
+ * feature. Treat the returned object as read-only; clone before mutating.
87
+ *
88
+ * @type {Readonly<SessionCapabilities>}
89
+ */
90
+ export const DEFAULT_SESSION_CAPABILITIES = Object.freeze({ goal: false });
91
+ /**
92
+ * Resolve a session's capability snapshot, falling back to
93
+ * {@link DEFAULT_SESSION_CAPABILITIES}.
94
+ *
95
+ * @param {unknown} session
96
+ * @returns {SessionCapabilities}
97
+ */
98
+ export function resolveSessionCapabilities(session) {
99
+ if (session && typeof session === "object") {
100
+ const obj = /** @type {Record<string, unknown>} */ (session);
101
+ if (typeof obj.getCapabilities === "function") {
102
+ const raw = /** @type {() => unknown} */ (obj.getCapabilities).call(session);
103
+ if (raw && typeof raw === "object") {
104
+ return { ...DEFAULT_SESSION_CAPABILITIES, ...raw };
105
+ }
106
+ }
107
+ const constructor = /** @type {{ capabilities?: unknown }} */ ((session.constructor && typeof session.constructor === "function"
108
+ ? session.constructor
109
+ : null) || {});
110
+ if (constructor && constructor.capabilities && typeof constructor.capabilities === "object") {
111
+ return { ...DEFAULT_SESSION_CAPABILITIES, ...constructor.capabilities };
112
+ }
113
+ }
114
+ return { ...DEFAULT_SESSION_CAPABILITIES };
115
+ }
5
116
  export function normalizeLogger(logger) {
6
117
  if (typeof logger === "function") {
7
118
  return { log: logger };
@@ -4,6 +4,7 @@ export class CodexAppServerTransport extends EventEmitter<[never]> {
4
4
  logger: any;
5
5
  cwd: any;
6
6
  command: string;
7
+ enableGoals: boolean;
7
8
  args: string[];
8
9
  env: any;
9
10
  ignoreCodexApiKey: boolean;
@@ -3,6 +3,49 @@ import { spawn } from "node:child_process";
3
3
  import readline from "node:readline";
4
4
  import { emitLog, normalizeLogger, parseCommandParts, sanitizeForLog, serializeError, } from "../shared.js";
5
5
  const DEFAULT_CODEX_APP_SERVER_COMMAND = "codex app-server --listen stdio://";
6
+ /**
7
+ * Add `--enable goals` to the codex app-server spawn args when goal mode is
8
+ * requested. Dynamic enablement via `experimentalFeature/enablement/set` is
9
+ * NOT supported by codex, so the feature must be enabled at boot. Idempotent:
10
+ * does not double-add when the user already passed any of the supported
11
+ * spellings (`--enable goals`, `--enable=goals`, `--enable=goals,foo`) in the
12
+ * command line.
13
+ *
14
+ * @param {string[]} args
15
+ * @param {boolean} enableGoals
16
+ * @returns {string[]}
17
+ */
18
+ function injectEnableGoalsArgs(args, enableGoals) {
19
+ const normalized = Array.isArray(args) ? [...args] : [];
20
+ if (!enableGoals) {
21
+ return normalized;
22
+ }
23
+ for (let index = 0; index < normalized.length; index += 1) {
24
+ const token = normalized[index];
25
+ if (typeof token !== "string") {
26
+ continue;
27
+ }
28
+ // Two-token form: ["--enable", "goals"]
29
+ if (token === "--enable" && normalized[index + 1] === "goals") {
30
+ return normalized;
31
+ }
32
+ // Equals form: "--enable=goals" or comma list "--enable=goals,foo"
33
+ if (token.startsWith("--enable=")) {
34
+ const value = token.slice("--enable=".length);
35
+ const features = value
36
+ .split(",")
37
+ .map((entry) => entry.trim().toLowerCase())
38
+ .filter(Boolean);
39
+ if (features.includes("goals")) {
40
+ return normalized;
41
+ }
42
+ }
43
+ }
44
+ const listenIndex = normalized.indexOf("--listen");
45
+ const insertAt = listenIndex >= 0 ? listenIndex : normalized.length;
46
+ normalized.splice(insertAt, 0, "--enable", "goals");
47
+ return normalized;
48
+ }
6
49
  function createRpcError(payload) {
7
50
  const message = String(payload?.message || payload?.error?.message || "Codex app-server request failed");
8
51
  const error = new Error(message);
@@ -34,7 +77,8 @@ export class CodexAppServerTransport extends EventEmitter {
34
77
  throw new Error("Invalid codex app-server command");
35
78
  }
36
79
  this.command = command;
37
- this.args = args;
80
+ this.enableGoals = options.enableGoals === true;
81
+ this.args = injectEnableGoalsArgs(args, this.enableGoals);
38
82
  this.env = options.env && typeof options.env === "object" ? { ...options.env } : {};
39
83
  this.ignoreCodexApiKey = options.ignoreCodexApiKey === true;
40
84
  this.child = null;
package/dist/worker.js CHANGED
@@ -1,7 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import readline from "node:readline";
3
3
  import { createLocalAiSession } from "./session-factory.js";
4
- import { serializeError } from "./shared.js";
4
+ import { DEFAULT_SESSION_CAPABILITIES, resolveSessionCapabilities, serializeError, } from "./shared.js";
5
+ function buildReadySnapshot(aiSession) {
6
+ const snapshot = typeof aiSession?.getSnapshot === "function" ? aiSession.getSnapshot() : null;
7
+ const base = snapshot && typeof snapshot === "object" ? { ...snapshot } : {};
8
+ // Always include a capabilities field so the parent process can do the
9
+ // declarative `capabilities.goal === false` short-circuit without needing
10
+ // to inspect method names through the IPC boundary.
11
+ if (!base.capabilities || typeof base.capabilities !== "object") {
12
+ base.capabilities = resolveSessionCapabilities(aiSession);
13
+ }
14
+ else {
15
+ base.capabilities = { ...DEFAULT_SESSION_CAPABILITIES, ...base.capabilities };
16
+ }
17
+ return base;
18
+ }
5
19
  let session = null;
6
20
  let sessionCreated = false;
7
21
  function send(payload) {
@@ -48,7 +62,7 @@ async function handleCreate(message) {
48
62
  attachSessionEvents(session);
49
63
  send({
50
64
  type: "ready",
51
- snapshot: typeof session.getSnapshot === "function" ? session.getSnapshot() : null,
65
+ snapshot: buildReadySnapshot(session),
52
66
  workerPid: process.pid,
53
67
  workerProcessPid: process.pid,
54
68
  });
@@ -62,8 +76,8 @@ async function handleRequest(message) {
62
76
  throw new Error(`Unsupported worker method: ${method}`);
63
77
  }
64
78
  const args = Array.isArray(message.args) ? [...message.args] : [];
65
- if (method === "runTurn") {
66
- const prompt = args[0];
79
+ if (method === "runTurn" || method === "runGoal") {
80
+ const firstArg = args[0];
67
81
  const options = args[1] && typeof args[1] === "object" ? { ...args[1] } : {};
68
82
  options.onProgress = (payload) => {
69
83
  send({
@@ -72,7 +86,7 @@ async function handleRequest(message) {
72
86
  payload,
73
87
  });
74
88
  };
75
- args[0] = prompt;
89
+ args[0] = firstArg;
76
90
  args[1] = options;
77
91
  }
78
92
  const result = await session[method](...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -18,7 +18,9 @@
18
18
  },
19
19
  "scripts": {
20
20
  "build": "tsc -p tsconfig.json",
21
- "test": "node --test",
21
+ "test": "npm run test:js && npm run test:manager",
22
+ "test:js": "node --import tsx --test test/*.test.js",
23
+ "test:manager": "node --import tsx --test test/manager/*.test.ts",
22
24
  "prepublishOnly": "npm run build"
23
25
  },
24
26
  "dependencies": {
@@ -26,11 +28,16 @@
26
28
  "@github/copilot-sdk": "^0.2.2",
27
29
  "@opencode-ai/sdk": "^1.2.25",
28
30
  "js-yaml": "^4.1.1",
31
+ "yaml": "^2.5.1",
29
32
  "zod": "^4.1.5"
30
33
  },
34
+ "optionalDependencies": {
35
+ "@love-moon/chat-web": "^0.4.0"
36
+ },
31
37
  "devDependencies": {
32
38
  "@types/node": "^22.10.2",
39
+ "tsx": "^4.20.6",
33
40
  "typescript": "^5.6.3"
34
41
  },
35
- "gitCommitId": "03b4582"
42
+ "gitCommitId": "2d1526c"
36
43
  }