@diffexai/diffex 0.2.1 → 0.2.3

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 (80) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +5 -725
  3. package/THIRD-PARTY-NOTICES.txt +6 -2
  4. package/dist/AGENTS.md +28 -0
  5. package/dist/cli/session-selection.d.ts +7 -0
  6. package/dist/cli/session-selection.js +181 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.js +2 -0
  9. package/dist/core/extensions/factory.d.ts +21 -0
  10. package/dist/core/extensions/factory.js +254 -0
  11. package/dist/core/extensions/loader.d.ts +2 -10
  12. package/dist/core/extensions/loader.js +2 -251
  13. package/dist/core/prompt-templates.js +6 -1
  14. package/dist/core/resource-loader.js +7 -2
  15. package/dist/core/system-prompt.js +9 -8
  16. package/dist/core/tools/subagents.js +1 -0
  17. package/dist/main.js +3 -176
  18. package/dist/node_modules/@diffexai/diffex-agent-core/THIRD-PARTY-NOTICES.txt +6 -2
  19. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
  20. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
  21. package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -6
  22. package/dist/node_modules/@diffexai/diffex-ai/THIRD-PARTY-NOTICES.txt +6 -2
  23. package/dist/node_modules/@diffexai/diffex-ai/dist/cli.js +0 -0
  24. package/dist/node_modules/@diffexai/diffex-ai/dist/image-models.generated.d.ts +180 -0
  25. package/dist/node_modules/@diffexai/diffex-ai/dist/image-models.generated.js +182 -2
  26. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
  27. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/kimi-coding.json +1 -1
  28. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/nvidia.json +1 -1
  29. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
  30. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/qwen-token-plan-cn.json +1 -1
  31. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  32. package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
  33. package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +9 -9
  34. package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -6
  35. package/dist/node_modules/@diffexai/diffex-client/THIRD-PARTY-NOTICES.txt +6 -2
  36. package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
  37. package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
  38. package/dist/node_modules/@diffexai/diffex-client/package.json +1 -6
  39. package/dist/node_modules/@diffexai/diffex-harness-state/THIRD-PARTY-NOTICES.txt +6 -2
  40. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
  41. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
  42. package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -6
  43. package/dist/node_modules/@diffexai/diffex-protocol/THIRD-PARTY-NOTICES.txt +6 -2
  44. package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
  45. package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
  46. package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -6
  47. package/dist/node_modules/@diffexai/diffex-telemetry/THIRD-PARTY-NOTICES.txt +6 -2
  48. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
  49. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
  50. package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -6
  51. package/dist/node_modules/@diffexai/diffex-tui/THIRD-PARTY-NOTICES.txt +6 -2
  52. package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
  53. package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
  54. package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -6
  55. package/distribution-components.json +13 -13
  56. package/distribution-files.json +66 -74
  57. package/docs/installation.md +118 -0
  58. package/docs/quickstart.md +7 -24
  59. package/docs/usage.md +10 -4
  60. package/npm-shrinkwrap.json +2 -2
  61. package/package.json +36 -10
  62. package/release/distribution-manifest.json +7 -6
  63. package/release/install-package-lock.json +5 -5
  64. package/release/install-package.json +2 -2
  65. package/NOTICE +0 -6
  66. package/dist/node_modules/@diffexai/diffex-agent-core/NOTICE +0 -6
  67. package/dist/node_modules/@diffexai/diffex-ai/NOTICE +0 -6
  68. package/dist/node_modules/@diffexai/diffex-client/NOTICE +0 -6
  69. package/dist/node_modules/@diffexai/diffex-harness-state/NOTICE +0 -6
  70. package/dist/node_modules/@diffexai/diffex-protocol/NOTICE +0 -6
  71. package/dist/node_modules/@diffexai/diffex-telemetry/NOTICE +0 -6
  72. package/dist/node_modules/@diffexai/diffex-tui/NOTICE +0 -6
  73. package/docs/development.md +0 -71
  74. package/docs/docs.json +0 -160
  75. package/docs/images/doom-extension.png +0 -0
  76. package/docs/images/exy.png +0 -0
  77. package/docs/index.md +0 -78
  78. package/docs/invocation-traces.md +0 -82
  79. package/docs/termux.md +0 -127
  80. package/docs/windows.md +0 -17
@@ -25,9 +25,9 @@ import * as _bundledDiffexCodingAgent from "../../index.js";
25
25
  import { resolvePath } from "../../utils/paths.js";
26
26
  import { readDiffexManifest } from "../diffex-manifest.js";
27
27
  import { createEventBus } from "../event-bus.js";
28
- import { execCommand } from "../exec.js";
29
- import { createSyntheticSourceInfo } from "../source-info.js";
30
28
  import { time } from "../timings.js";
29
+ import { createExtension, createExtensionAPI, createExtensionRuntime } from "./factory.js";
30
+ export { createExtensionRuntime, loadExtensionFromFactory } from "./factory.js";
31
31
  /** Modules available to extensions via virtualModules (for compiled Bun binary) */
32
32
  const VIRTUAL_MODULES = {
33
33
  typebox: _bundledTypebox,
@@ -112,223 +112,6 @@ function useExtensionCacheCwd(cwd) {
112
112
  extensionCacheCwd = resolvedCwd;
113
113
  return { cwd: resolvedCwd, generation: extensionCacheGeneration };
114
114
  }
115
- /**
116
- * Create a runtime with throwing stubs for action methods.
117
- * Runner.bindCore() replaces these with real implementations.
118
- */
119
- export function createExtensionRuntime() {
120
- const notInitialized = () => {
121
- throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
122
- };
123
- const state = {};
124
- const eventBusUnsubscribers = new Set();
125
- const assertActive = () => {
126
- if (state.staleMessage) {
127
- throw new Error(state.staleMessage);
128
- }
129
- };
130
- const runtime = {
131
- sendMessage: notInitialized,
132
- sendUserMessage: notInitialized,
133
- appendEntry: notInitialized,
134
- setSessionName: notInitialized,
135
- getSessionName: notInitialized,
136
- setLabel: notInitialized,
137
- getActiveTools: notInitialized,
138
- getAllTools: notInitialized,
139
- setActiveTools: notInitialized,
140
- // registerTool() is valid during extension load; refresh is only needed post-bind.
141
- refreshTools: () => { },
142
- getCommands: notInitialized,
143
- setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
144
- getThinkingLevel: notInitialized,
145
- setThinkingLevel: notInitialized,
146
- flagValues: new Map(),
147
- pendingProviderRegistrations: [],
148
- pendingNativeProviderRegistrations: [],
149
- assertActive,
150
- invalidate: (message) => {
151
- if (state.staleMessage)
152
- return;
153
- state.staleMessage =
154
- message ??
155
- "This extension ctx is stale after session replacement or reload. Do not use a captured Diffex API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().";
156
- for (const unsubscribe of eventBusUnsubscribers)
157
- unsubscribe();
158
- eventBusUnsubscribers.clear();
159
- },
160
- trackEventBusSubscription: (unsubscribe) => {
161
- let active = true;
162
- const trackedUnsubscribe = () => {
163
- if (!active)
164
- return;
165
- active = false;
166
- eventBusUnsubscribers.delete(trackedUnsubscribe);
167
- unsubscribe();
168
- };
169
- eventBusUnsubscribers.add(trackedUnsubscribe);
170
- return trackedUnsubscribe;
171
- },
172
- // Pre-bind: queue registrations so bindCore() can flush them once the
173
- // model registry is available. bindCore() replaces both with direct calls.
174
- registerProvider: (name, config, extensionPath = "<unknown>") => {
175
- runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
176
- },
177
- registerNativeProvider: (provider, extensionPath = "<unknown>") => {
178
- runtime.pendingNativeProviderRegistrations.push({ provider, extensionPath });
179
- },
180
- unregisterProvider: (name) => {
181
- runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);
182
- runtime.pendingNativeProviderRegistrations = runtime.pendingNativeProviderRegistrations.filter((r) => r.provider.id !== name);
183
- },
184
- };
185
- return runtime;
186
- }
187
- /**
188
- * Create the ExtensionAPI for an extension.
189
- * Registration methods write to the extension object.
190
- * Action methods delegate to the shared runtime.
191
- */
192
- function createExtensionAPI(extension, runtime, cwd, eventBus) {
193
- const api = {
194
- // Registration methods - write to extension
195
- on(event, handler) {
196
- runtime.assertActive();
197
- const list = extension.handlers.get(event) ?? [];
198
- list.push(handler);
199
- extension.handlers.set(event, list);
200
- },
201
- registerTool(tool) {
202
- runtime.assertActive();
203
- extension.tools.set(tool.name, {
204
- definition: tool,
205
- sourceInfo: extension.sourceInfo,
206
- });
207
- runtime.refreshTools();
208
- },
209
- registerCommand(name, options) {
210
- runtime.assertActive();
211
- extension.commands.set(name, {
212
- name,
213
- sourceInfo: extension.sourceInfo,
214
- ...options,
215
- });
216
- },
217
- registerShortcut(shortcut, options) {
218
- runtime.assertActive();
219
- extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
220
- },
221
- registerFlag(name, options) {
222
- runtime.assertActive();
223
- extension.flags.set(name, { name, extensionPath: extension.path, ...options });
224
- if (options.default !== undefined && !runtime.flagValues.has(name)) {
225
- runtime.flagValues.set(name, options.default);
226
- }
227
- },
228
- registerMessageRenderer(customType, renderer) {
229
- runtime.assertActive();
230
- extension.messageRenderers.set(customType, renderer);
231
- },
232
- registerMarkdownTransformer(transformer) {
233
- runtime.assertActive();
234
- extension.markdownTransformer = transformer;
235
- },
236
- registerEntryRenderer(customType, renderer) {
237
- runtime.assertActive();
238
- extension.entryRenderers ??= new Map();
239
- extension.entryRenderers.set(customType, renderer);
240
- },
241
- // Flag access - checks extension registered it, reads from runtime
242
- getFlag(name) {
243
- runtime.assertActive();
244
- if (!extension.flags.has(name))
245
- return undefined;
246
- return runtime.flagValues.get(name);
247
- },
248
- // Action methods - delegate to shared runtime
249
- sendMessage(message, options) {
250
- runtime.assertActive();
251
- runtime.sendMessage(message, options);
252
- },
253
- sendUserMessage(content, options) {
254
- runtime.assertActive();
255
- runtime.sendUserMessage(content, options);
256
- },
257
- appendEntry(customType, data) {
258
- runtime.assertActive();
259
- runtime.appendEntry(customType, data);
260
- },
261
- setSessionName(name) {
262
- runtime.assertActive();
263
- runtime.setSessionName(name);
264
- },
265
- getSessionName() {
266
- runtime.assertActive();
267
- return runtime.getSessionName();
268
- },
269
- setLabel(entryId, label) {
270
- runtime.assertActive();
271
- runtime.setLabel(entryId, label);
272
- },
273
- exec(command, args, options) {
274
- runtime.assertActive();
275
- return execCommand(command, args, options?.cwd ?? cwd, options);
276
- },
277
- getActiveTools() {
278
- runtime.assertActive();
279
- return runtime.getActiveTools();
280
- },
281
- getAllTools() {
282
- runtime.assertActive();
283
- return runtime.getAllTools();
284
- },
285
- setActiveTools(toolNames) {
286
- runtime.assertActive();
287
- runtime.setActiveTools(toolNames);
288
- },
289
- getCommands() {
290
- runtime.assertActive();
291
- return runtime.getCommands();
292
- },
293
- setModel(model) {
294
- runtime.assertActive();
295
- return runtime.setModel(model);
296
- },
297
- getThinkingLevel() {
298
- runtime.assertActive();
299
- return runtime.getThinkingLevel();
300
- },
301
- setThinkingLevel(level) {
302
- runtime.assertActive();
303
- runtime.setThinkingLevel(level);
304
- },
305
- registerProvider(providerOrName, config) {
306
- runtime.assertActive();
307
- if (typeof providerOrName === "string") {
308
- if (!config)
309
- throw new Error("Provider config is required when registering by name");
310
- runtime.registerProvider(providerOrName, config, extension.path);
311
- return;
312
- }
313
- runtime.registerNativeProvider(providerOrName, extension.path);
314
- },
315
- unregisterProvider(name) {
316
- runtime.assertActive();
317
- runtime.unregisterProvider(name, extension.path);
318
- },
319
- events: {
320
- emit(channel, data) {
321
- runtime.assertActive();
322
- eventBus.emit(channel, data);
323
- },
324
- on(channel, handler) {
325
- runtime.assertActive();
326
- return runtime.trackEventBusSubscription(eventBus.on(channel, handler));
327
- },
328
- },
329
- };
330
- return api;
331
- }
332
115
  function isCurrentCacheToken(cacheToken) {
333
116
  return (cacheToken !== undefined &&
334
117
  extensionCacheCwd === cacheToken.cwd &&
@@ -361,27 +144,6 @@ async function loadExtensionModule(extensionPath, cacheToken) {
361
144
  }
362
145
  return factory;
363
146
  }
364
- /**
365
- * Create an Extension object with empty collections.
366
- */
367
- function createExtension(extensionPath, resolvedPath) {
368
- const source = extensionPath.startsWith("<") && extensionPath.endsWith(">")
369
- ? extensionPath.slice(1, -1).split(":")[0] || "temporary"
370
- : "local";
371
- const baseDir = extensionPath.startsWith("<") ? undefined : path.dirname(resolvedPath);
372
- return {
373
- path: extensionPath,
374
- resolvedPath,
375
- sourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),
376
- handlers: new Map(),
377
- tools: new Map(),
378
- messageRenderers: new Map(),
379
- entryRenderers: new Map(),
380
- commands: new Map(),
381
- flags: new Map(),
382
- shortcuts: new Map(),
383
- };
384
- }
385
147
  async function loadExtension(extensionPath, cwd, eventBus, runtime, cacheToken) {
386
148
  const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });
387
149
  try {
@@ -401,17 +163,6 @@ async function loadExtension(extensionPath, cwd, eventBus, runtime, cacheToken)
401
163
  return { extension: null, error: `Failed to load extension: ${message}` };
402
164
  }
403
165
  }
404
- /**
405
- * Create an Extension from an inline factory function.
406
- */
407
- export async function loadExtensionFromFactory(factory, cwd, eventBus, runtime, extensionPath = "<inline>") {
408
- const extension = createExtension(extensionPath, extensionPath);
409
- const resolvedCwd = resolvePath(cwd);
410
- const api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);
411
- await factory(api);
412
- time(`${extensionPath} factory`, "extensions");
413
- return extension;
414
- }
415
166
  /**
416
167
  * Load extensions from paths.
417
168
  */
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
2
2
  import { basename, dirname, join, resolve, sep } from "path";
3
- import { CONFIG_DIR_NAME } from "../config.js";
3
+ import { CONFIG_DIR_NAME, IS_PRODUCTION_DISTRIBUTION } from "../config.js";
4
4
  import { parseFrontmatter } from "../utils/frontmatter.js";
5
5
  import { resolvePath } from "../utils/paths.js";
6
6
  import { createSyntheticSourceInfo } from "./source-info.js";
@@ -83,6 +83,8 @@ function loadTemplateFromFile(filePath, sourceInfo) {
83
83
  const rawContent = readFileSync(filePath, "utf-8");
84
84
  const { frontmatter, body } = parseFrontmatter(rawContent);
85
85
  const name = basename(filePath).replace(/\.md$/, "");
86
+ if (IS_PRODUCTION_DISTRIBUTION && name === "cl")
87
+ return null;
86
88
  // Get description from frontmatter or first non-empty line
87
89
  let description = frontmatter.description || "";
88
90
  if (!description) {
@@ -225,6 +227,9 @@ export function expandPromptTemplate(text, templates) {
225
227
  if (!match)
226
228
  return text;
227
229
  const templateName = match[1];
230
+ if (IS_PRODUCTION_DISTRIBUTION && templateName === "cl") {
231
+ throw new Error("/cl is unavailable in production Diffex installations.");
232
+ }
228
233
  const argsString = match[2] ?? "";
229
234
  const template = templates.find((t) => t.name === templateName);
230
235
  if (template) {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve, sep } from "node:path";
3
3
  import chalk from "chalk";
4
- import { CONFIG_DIR_NAME } from "../config.js";
4
+ import { CONFIG_DIR_NAME, getPackageDir, IS_PRODUCTION_DISTRIBUTION } from "../config.js";
5
5
  import { loadThemeFromPath } from "../modes/interactive/theme/theme.js";
6
6
  import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.js";
7
7
  import { createEventBus } from "./event-bus.js";
@@ -83,8 +83,13 @@ export function loadProjectContextFiles(options) {
83
83
  const resolvedAgentDir = resolvePath(options.agentDir);
84
84
  const contextFiles = [];
85
85
  const seenPaths = new Set();
86
+ if (IS_PRODUCTION_DISTRIBUTION) {
87
+ const path = join(getPackageDir(), "dist", "AGENTS.md");
88
+ contextFiles.push({ path, content: readFileSync(path, "utf-8") });
89
+ seenPaths.add(path);
90
+ }
86
91
  const globalContext = loadContextFileFromDir(resolvedAgentDir);
87
- if (globalContext) {
92
+ if (globalContext && !seenPaths.has(globalContext.path)) {
88
93
  contextFiles.push(globalContext);
89
94
  seenPaths.add(globalContext.path);
90
95
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * System prompt construction and project context loading
3
3
  */
4
- import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
4
+ import { existsSync } from "node:fs";
5
+ import { getDocsPath, getExamplesPath } from "../config.js";
5
6
  import { formatSkillsForPrompt } from "./skills.js";
6
7
  /** Build the system prompt with tools, guidelines, and context */
7
8
  export function buildSystemPrompt(options) {
@@ -37,7 +38,6 @@ export function buildSystemPrompt(options) {
37
38
  return prompt;
38
39
  }
39
40
  // Get absolute paths to documentation and examples
40
- const readmePath = getReadmePath();
41
41
  const docsPath = getDocsPath();
42
42
  const examplesPath = getExamplesPath();
43
43
  // Build tools list based on selected tools.
@@ -85,13 +85,14 @@ Guidelines:
85
85
  ${guidelines}
86
86
 
87
87
  Diffex documentation (read only when the user asks about Diffex itself, its SDK, extensions, themes, skills, or TUI):
88
- - Main documentation: ${readmePath}
88
+ ${existsSync(docsPath)
89
+ ? `- Product website: https://diffex.ai/
89
90
  - Additional docs: ${docsPath}
90
- - Examples: ${examplesPath} (extensions, custom tools, SDK)
91
- - When reading Diffex docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
92
- - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), Diffex packages (docs/packages.md), environment variables (docs/environment-variables.md)
93
- - When working on Diffex topics, read the docs and examples, and follow .md cross-references before implementing
94
- - Always read Diffex.md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
91
+ ${existsSync(examplesPath) ? `- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When reading Diffex docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory` : "- Resolve docs/... under Additional docs, not the current working directory; example source files are not included in this installation"}
92
+ - When asked about: extensions (docs/extensions.md), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), Diffex packages (docs/packages.md), environment variables (docs/environment-variables.md)
93
+ - When working on Diffex topics, read the docs and follow .md cross-references before implementing
94
+ - Always read Diffex.md files completely and follow links to related docs (e.g., tui.md for TUI API details)`
95
+ : "- Product website: https://diffex.ai/"}`;
95
96
  if (appendSection) {
96
97
  prompt += appendSection;
97
98
  }
@@ -104,6 +104,7 @@ export function createSpawnAgentTool(spawn, assertCanSpawn = () => { }) {
104
104
  label: "Spawn Agent",
105
105
  description: "Start an independent child agent asynchronously. The tool returns after acceptance, but the enclosing parent prompt remains active until coordinated child work is delivered.",
106
106
  promptGuidelines: [
107
+ "For broad research, audits, or implementation work with independent areas, use spawn_agent to delegate focused subtasks in parallel when available. Request file references and supporting evidence for research and audits, and assign clear, non-overlapping file ownership for implementation work.",
107
108
  "After spawning child agents, you may finish the current response. The session waits for the frozen child batch and automatically sends one hidden, name-sorted result message before the parent prompt settles.",
108
109
  ],
109
110
  parameters: spawnAgentSchema,
package/dist/main.js CHANGED
@@ -4,7 +4,6 @@
4
4
  * This file handles CLI argument parsing and translates them into
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
- import { createInterface } from "node:readline";
8
7
  import { modelsAreEqual } from "@diffexai/diffex-ai";
9
8
  import chalk from "chalk";
10
9
  import { parseArgs, printHelp } from "./cli/args.js";
@@ -15,7 +14,7 @@ import { processFileArguments } from "./cli/file-processor.js";
15
14
  import { buildInitialMessage } from "./cli/initial-message.js";
16
15
  import { listModels } from "./cli/list-models.js";
17
16
  import { createProjectTrustContext } from "./cli/project-trust.js";
18
- import { selectSession } from "./cli/session-picker.js";
17
+ import { applySessionName, createSessionManager, validateForkFlags, validateSessionIdFlags, } from "./cli/session-selection.js";
19
18
  import { shouldRunFirstTimeSetup, showFirstTimeSetup, showStartupSelector } from "./cli/startup-ui.js";
20
19
  import { APP_NAME, ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.js";
21
20
  import { createAgentSessionRuntime, } from "./core/agent-session-runtime.js";
@@ -32,7 +31,7 @@ import { ModelRuntime } from "./core/model-runtime.js";
32
31
  import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
33
32
  import { resolveProjectTrusted } from "./core/project-trust.js";
34
33
  import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
35
- import { assertValidSessionId, SessionManager } from "./core/session-manager.js";
34
+ import { SessionManager } from "./core/session-manager.js";
36
35
  import { SettingsManager } from "./core/settings-manager.js";
37
36
  import { printTimings, resetTimings, time } from "./core/timings.js";
38
37
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.js";
@@ -192,171 +191,6 @@ async function prepareInitialMessage(parsed, autoResizeImages, stdinContent) {
192
191
  stdinContent,
193
192
  });
194
193
  }
195
- /**
196
- * Resolve a session argument to a file path.
197
- * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
198
- */
199
- async function findLocalSessionByExactId(sessionId, cwd, sessionDir) {
200
- const localSessions = await SessionManager.list(cwd, sessionDir);
201
- const localMatch = localSessions.find((s) => s.id === sessionId);
202
- return localMatch ? { type: "local", path: localMatch.path } : undefined;
203
- }
204
- async function resolveSessionPath(sessionArg, cwd, sessionDir) {
205
- // If it looks like a file path, resolve it before handing it to the session manager.
206
- if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
207
- return { type: "path", path: resolvePath(sessionArg, cwd) };
208
- }
209
- // Try to match as session ID in current project first
210
- const localSessions = await SessionManager.list(cwd, sessionDir);
211
- const localMatch = localSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));
212
- if (localMatch) {
213
- return { type: "local", path: localMatch.path };
214
- }
215
- // Try global search across all projects
216
- const allSessions = await SessionManager.listAll(sessionDir);
217
- const globalMatch = allSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));
218
- if (globalMatch) {
219
- return { type: "global", path: globalMatch.path, cwd: globalMatch.cwd };
220
- }
221
- // Not found anywhere
222
- return { type: "not_found", arg: sessionArg };
223
- }
224
- /** Prompt user for yes/no confirmation */
225
- async function promptConfirm(message) {
226
- return new Promise((resolve) => {
227
- const rl = createInterface({
228
- input: process.stdin,
229
- output: process.stdout,
230
- });
231
- rl.question(`${message} [y/N] `, (answer) => {
232
- rl.close();
233
- resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
234
- });
235
- });
236
- }
237
- function validateForkFlags(parsed) {
238
- if (!parsed.fork)
239
- return;
240
- const conflictingFlags = [
241
- parsed.session ? "--session" : undefined,
242
- parsed.continue ? "--continue" : undefined,
243
- parsed.resume ? "--resume" : undefined,
244
- parsed.noSession ? "--no-session" : undefined,
245
- ].filter((flag) => flag !== undefined);
246
- if (conflictingFlags.length > 0) {
247
- console.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(", ")}`));
248
- process.exit(1);
249
- }
250
- }
251
- function validateSessionIdFlags(parsed) {
252
- if (parsed.sessionId === undefined)
253
- return;
254
- const conflictingFlags = [
255
- parsed.session ? "--session" : undefined,
256
- parsed.continue ? "--continue" : undefined,
257
- parsed.resume ? "--resume" : undefined,
258
- ].filter((flag) => flag !== undefined);
259
- if (conflictingFlags.length > 0) {
260
- console.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(", ")}`));
261
- process.exit(1);
262
- }
263
- try {
264
- assertValidSessionId(parsed.sessionId);
265
- }
266
- catch (error) {
267
- const message = error instanceof Error ? error.message : String(error);
268
- console.error(chalk.red(`Error: ${message}`));
269
- process.exit(1);
270
- }
271
- }
272
- function openSessionOrExit(path, sessionDir) {
273
- try {
274
- return SessionManager.open(path, sessionDir);
275
- }
276
- catch (error) {
277
- const message = error instanceof Error ? error.message : String(error);
278
- console.error(chalk.red(`Error: ${message}`));
279
- process.exit(1);
280
- }
281
- }
282
- function forkSessionOrExit(sourcePath, cwd, sessionDir, sessionId) {
283
- try {
284
- return SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });
285
- }
286
- catch (error) {
287
- const message = error instanceof Error ? error.message : String(error);
288
- console.error(chalk.red(`Error: ${message}`));
289
- process.exit(1);
290
- }
291
- }
292
- async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
293
- if (parsed.noSession || parsed.help || parsed.listModels !== undefined) {
294
- return SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);
295
- }
296
- if (parsed.fork) {
297
- if (parsed.sessionId) {
298
- const existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
299
- if (existingTarget) {
300
- console.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));
301
- process.exit(1);
302
- }
303
- }
304
- const resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);
305
- switch (resolved.type) {
306
- case "path":
307
- case "local":
308
- case "global":
309
- return forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);
310
- case "not_found":
311
- console.error(chalk.red(`No session found matching '${resolved.arg}'`));
312
- process.exit(1);
313
- }
314
- }
315
- if (parsed.session) {
316
- const resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);
317
- switch (resolved.type) {
318
- case "path":
319
- case "local":
320
- return openSessionOrExit(resolved.path, sessionDir);
321
- case "global": {
322
- console.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));
323
- const shouldFork = await promptConfirm("Fork this session into current directory?");
324
- if (!shouldFork) {
325
- console.log(chalk.dim("Aborted."));
326
- process.exit(0);
327
- }
328
- return forkSessionOrExit(resolved.path, cwd, sessionDir);
329
- }
330
- case "not_found":
331
- console.error(chalk.red(`No session found matching '${resolved.arg}'`));
332
- process.exit(1);
333
- }
334
- }
335
- if (parsed.resume) {
336
- try {
337
- const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress), settingsManager);
338
- if (!selectedPath) {
339
- console.log(chalk.dim("No session selected"));
340
- process.exit(0);
341
- }
342
- return SessionManager.open(selectedPath, sessionDir);
343
- }
344
- finally {
345
- stopThemeWatcher();
346
- }
347
- }
348
- if (parsed.continue) {
349
- return SessionManager.continueRecent(cwd, sessionDir);
350
- }
351
- if (parsed.sessionId) {
352
- const existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
353
- if (existingSession) {
354
- return SessionManager.open(existingSession.path, sessionDir);
355
- }
356
- console.error(chalk.yellow(`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`));
357
- }
358
- return SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });
359
- }
360
194
  function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRuntime, settingsManager) {
361
195
  const options = {};
362
196
  const diagnostics = [];
@@ -557,14 +391,7 @@ export async function main(args, options) {
557
391
  process.exit(1);
558
392
  }
559
393
  }
560
- if (parsed.name !== undefined) {
561
- const name = parsed.name.trim();
562
- if (!name) {
563
- console.error(chalk.red("Error: --name requires a non-empty value"));
564
- process.exit(1);
565
- }
566
- sessionManager.appendSessionInfo(name);
567
- }
394
+ applySessionName(parsed, sessionManager);
568
395
  const initialWorkspace = await resolveHarnessWorkspace(sessionManager.getCwd());
569
396
  const invocationTraceResult = await createInvocationTrace({
570
397
  agentDir: resolveEvolutionWorkspaceStateDirectory(agentDir, initialWorkspace.workspace),
@@ -1,2 +1,6 @@
1
- Diffex modifications copyright 2026 Luntraa, Inc.
2
- Pi-derived portions retain the complete MIT terms in LICENSE.
1
+ Diffex
2
+ Copyright 2026 Luntraa, Inc.
3
+
4
+ Where this distribution contains Pi Agent Harness-derived portions,
5
+ those portions retain their MIT terms and copyright notice in LICENSE.
6
+ The Apache-2.0 package declaration does not replace third-party terms.
@@ -1,8 +1,8 @@
1
1
  [
2
2
  {
3
3
  "name": "@diffexai/diffex-agent-core",
4
- "version": "0.2.1",
5
- "sourceCommit": "f2a661684af35fe6e9a3209c6f9720144e1fd423",
4
+ "version": "0.2.3",
5
+ "sourceCommit": "a9a2beea7798edd34c62e836d73a0d50b6260950",
6
6
  "license": "Apache-2.0",
7
7
  "portions": "Diffex contributions; Pi-derived portions retain MIT terms in LICENSE",
8
8
  "notice": "LICENSE",
@@ -10,12 +10,12 @@
10
10
  },
11
11
  {
12
12
  "name": "@diffexai/diffex-ai",
13
- "version": "^0.2.1",
13
+ "version": "^0.2.3",
14
14
  "distribution": "resolved separately by package manager; not bundled in this npm tarball"
15
15
  },
16
16
  {
17
17
  "name": "@diffexai/diffex-telemetry",
18
- "version": "^0.2.1",
18
+ "version": "^0.2.3",
19
19
  "distribution": "resolved separately by package manager; not bundled in this npm tarball"
20
20
  },
21
21
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "sourceCommit": "f2a661684af35fe6e9a3209c6f9720144e1fd423",
2
+ "sourceCommit": "a9a2beea7798edd34c62e836d73a0d50b6260950",
3
3
  "algorithm": "sha256",
4
4
  "files": [
5
5
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diffexai/diffex-agent-core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,11 +29,6 @@
29
29
  ],
30
30
  "author": "Mario Zechner",
31
31
  "license": "Apache-2.0",
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/diffexai/diffex.git",
35
- "directory": "packages/agent"
36
- },
37
32
  "engines": {
38
33
  "node": ">=22.19.0"
39
34
  },
@@ -1,2 +1,6 @@
1
- Diffex modifications copyright 2026 Luntraa, Inc.
2
- Pi-derived portions retain the complete MIT terms in LICENSE.
1
+ Diffex
2
+ Copyright 2026 Luntraa, Inc.
3
+
4
+ Where this distribution contains Pi Agent Harness-derived portions,
5
+ those portions retain their MIT terms and copyright notice in LICENSE.
6
+ The Apache-2.0 package declaration does not replace third-party terms.
File without changes