@linzumi/cli 0.0.76-beta → 0.0.78-beta

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +3255 -453
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -376,12 +376,7 @@ function appendDownloadedAttachmentContext(input, attachments, options = {}) {
376
376
  "Do not mention that you are using the upload tool. Do not print an upload manifest or upload footer in the message.",
377
377
  "The MCP upload tool is the only supported path for attaching generated files to Linzumi messages."
378
378
  ];
379
- return [
380
- input,
381
- ...attachmentContext,
382
- "",
383
- ...uploadInstructions
384
- ].join("\n");
379
+ return [input, ...attachmentContext, "", ...uploadInstructions].join("\n");
385
380
  }
386
381
  function codexImageInputItemsForDownloadedAttachments(attachments) {
387
382
  return attachments.flatMap(
@@ -659,7 +654,11 @@ async function commanderAttachmentUploadRoots(cwdRealPath) {
659
654
  const roots = [
660
655
  { path: cwdRealPath, label: "runner cwd" }
661
656
  ];
662
- const downloadsPath = resolve(homedir(), "Downloads");
657
+ const configuredHome = process.env.HOME?.trim();
658
+ const downloadsPath = resolve(
659
+ configuredHome === void 0 || configuredHome === "" ? homedir() : configuredHome,
660
+ "Downloads"
661
+ );
663
662
  try {
664
663
  const downloadsRealPath = await realpath(downloadsPath);
665
664
  if (!roots.some((root) => root.path === downloadsRealPath)) {
@@ -9603,7 +9602,7 @@ function defaultCliAuditLogFile() {
9603
9602
  return override === void 0 || override === "" ? join4(homedir4(), ".linzumi", "logs", "command-events.jsonl") : override;
9604
9603
  }
9605
9604
  function defaultRunnerLogFile() {
9606
- return join4(homedir4(), ".linzumi", "logs", "runner-events.jsonl");
9605
+ return join4(homedir4(), ".linzumi", "logs", "linzumi-runner.log");
9607
9606
  }
9608
9607
  function redactForCliLog(value) {
9609
9608
  return redactObject(value);
@@ -9773,7 +9772,8 @@ function linzumiMcpServerConfig(options) {
9773
9772
  ...options.cwd === void 0 ? [] : ["--cwd", options.cwd],
9774
9773
  ...options.threadId === void 0 ? [] : ["--thread-id", options.threadId],
9775
9774
  "--mode",
9776
- options.operatingMode ?? "text"
9775
+ options.operatingMode ?? "text",
9776
+ ...options.toolScope === void 0 || options.toolScope === "all" ? [] : ["--tool-scope", options.toolScope]
9777
9777
  ],
9778
9778
  env: {
9779
9779
  ...options.accessToken === void 0 ? {} : { LINZUMI_MCP_ACCESS_TOKEN: options.accessToken }
@@ -10726,6 +10726,9 @@ async function validateLocalRunnerToken(args) {
10726
10726
  if (args.channelSlug !== void 0) {
10727
10727
  url.searchParams.set("channel", args.channelSlug);
10728
10728
  }
10729
+ for (const scope of args.requiredScopes ?? []) {
10730
+ url.searchParams.append("required_scope", scope);
10731
+ }
10729
10732
  const response = await fetch(url, {
10730
10733
  method: "GET",
10731
10734
  headers: { authorization: `Bearer ${args.accessToken}` }
@@ -13578,7 +13581,7 @@ async function resolveEditorRuntime(options) {
13578
13581
  fetchImpl: options.fetchImpl ?? fetch
13579
13582
  });
13580
13583
  if (!manifest.ok) {
13581
- return unavailable("manifest_unavailable");
13584
+ return unavailable(manifest.reason);
13582
13585
  }
13583
13586
  const cacheRoot = options.cacheRoot ?? defaultEditorRuntimeCacheRoot();
13584
13587
  const installed = installedRuntime(cacheRoot, manifest.manifest);
@@ -13670,17 +13673,24 @@ async function fetchApprovedManifest(args) {
13670
13673
  headers: { authorization: `Bearer ${args.token}` }
13671
13674
  }).catch(() => void 0);
13672
13675
  if (response === void 0) {
13673
- return { ok: false };
13676
+ return { ok: false, reason: "manifest_unavailable" };
13674
13677
  }
13675
13678
  if (response.status !== 200) {
13676
- return { ok: false };
13679
+ return {
13680
+ ok: false,
13681
+ reason: response.status === 401 ? await unauthorizedManifestReason(response) : "manifest_unavailable"
13682
+ };
13677
13683
  }
13678
13684
  const body = await response.json();
13679
13685
  if (!isJsonObject(body) || body.ok !== true || !isJsonObject(body.runtime)) {
13680
- return { ok: false };
13686
+ return { ok: false, reason: "manifest_unavailable" };
13681
13687
  }
13682
13688
  return normalizeManifest(body.runtime);
13683
13689
  }
13690
+ async function unauthorizedManifestReason(response) {
13691
+ const body = await response.json().catch(() => void 0);
13692
+ return isJsonObject(body) && body.error === "token_expired" ? "auth_expired" : "auth_unauthorized";
13693
+ }
13684
13694
  function normalizeManifest(value) {
13685
13695
  const version = nonEmptyString(value.version);
13686
13696
  const platform = nonEmptyString(value.platform);
@@ -13691,7 +13701,7 @@ function normalizeManifest(value) {
13691
13701
  const manifestPath = nonEmptyString(value.manifestPath) ?? "linzumi-editor-runtime.json";
13692
13702
  const assets = normalizeRuntimeAssets(value.assets);
13693
13703
  if (version === void 0 || platform === void 0 || archiveUrl === void 0 || archiveSha256 === void 0 || codeServerVersion === void 0 || assets === void 0) {
13694
- return { ok: false };
13704
+ return { ok: false, reason: "manifest_unavailable" };
13695
13705
  }
13696
13706
  return {
13697
13707
  ok: true,
@@ -14265,6 +14275,20 @@ function assertRunnerConnectionDependencies(status) {
14265
14275
  );
14266
14276
  }
14267
14277
  if (status.editorRuntime?.status === "unavailable" || status.codeServer?.available === false && status.codeServer.reason !== "not_configured") {
14278
+ if (status.editorRuntime?.status === "unavailable") {
14279
+ switch (status.editorRuntime.reason) {
14280
+ case "auth_expired":
14281
+ throw new Error(
14282
+ "Linzumi authentication expired. Sign in again, then reconnect the local runner."
14283
+ );
14284
+ case "auth_unauthorized":
14285
+ throw new Error(
14286
+ "Linzumi authentication was rejected. Sign in again, then reconnect the local runner."
14287
+ );
14288
+ default:
14289
+ break;
14290
+ }
14291
+ }
14268
14292
  throw new Error(
14269
14293
  "The Linzumi editor runtime is not available. Reconnect when the runtime update finishes or pass --code-server-bin <path> for a custom development runtime."
14270
14294
  );
@@ -14633,7 +14657,7 @@ var linzumiCliVersion, linzumiCliVersionText;
14633
14657
  var init_version = __esm({
14634
14658
  "src/version.ts"() {
14635
14659
  "use strict";
14636
- linzumiCliVersion = "0.0.76-beta";
14660
+ linzumiCliVersion = "0.0.78-beta";
14637
14661
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
14638
14662
  }
14639
14663
  });
@@ -14939,6 +14963,39 @@ function updateRunnerConsoleDashboard(state, event, payload, nowMs) {
14939
14963
  state.lastUpdateAtMs = nowMs;
14940
14964
  const previousJobKey = latestDashboardJob(state)?.key;
14941
14965
  switch (event) {
14966
+ case "onboarding.discovery_agents_started":
14967
+ state.browserUrl = stringValue4(payload.kandanUrl);
14968
+ state.workspace = stringValue4(payload.workspace);
14969
+ state.runnerId = stringValue4(payload.runnerId);
14970
+ break;
14971
+ case "onboarding.discovery_status": {
14972
+ const phase = stringValue4(payload.phase);
14973
+ const status = stringValue4(payload.status);
14974
+ const metadata = objectValue5(payload.metadata);
14975
+ const workerId = stringValue4(metadata?.worker_id);
14976
+ if (phase !== void 0 && status !== void 0) {
14977
+ const isConversationPhase = phase === "conversations";
14978
+ const key = isConversationPhase ? phase : workerId ?? phase;
14979
+ const previous = state.discovery.get(key);
14980
+ const mergedMetadata = isConversationPhase ? mergeConversationImportMetadata(previous?.metadata, metadata) : metadata;
14981
+ state.discovery.set(key, {
14982
+ key,
14983
+ phase,
14984
+ status: isConversationPhase ? conversationDiscoveryMergedStatus(previous, status, metadata) : status,
14985
+ placesSearched: Math.max(
14986
+ previous?.placesSearched ?? 0,
14987
+ numberValue(payload.places_searched) ?? 0
14988
+ ),
14989
+ message: stringValue4(payload.message) ?? (isConversationPhase ? previous?.message : void 0),
14990
+ currentPlace: stringValue4(payload.current_place) ?? (isConversationPhase ? previous?.currentPlace : void 0),
14991
+ currentSource: stringValue4(payload.current_source) ?? (isConversationPhase ? previous?.currentSource : void 0),
14992
+ lastError: stringValue4(payload.last_error),
14993
+ metadata: mergedMetadata,
14994
+ updatedAtMs: nowMs
14995
+ });
14996
+ }
14997
+ break;
14998
+ }
14942
14999
  case "kandan.message_queued":
14943
15000
  case "codex.turn_starting": {
14944
15001
  const job = dashboardJobForPayload(state, payload, nowMs);
@@ -15007,12 +15064,15 @@ function updateRunnerConsoleDashboard(state, event, payload, nowMs) {
15007
15064
  rememberRawLine(state, event, payload, previousJobKey);
15008
15065
  }
15009
15066
  function renderRunnerConsoleDashboard(state, nowMs) {
15010
- if (state.jobs.size === 0 && state.tokenUsage === void 0 && state.rateLimit === void 0 && state.creditExhaustion === void 0) {
15067
+ if (state.jobs.size === 0 && state.discovery.size === 0 && state.tokenUsage === void 0 && state.rateLimit === void 0 && state.creditExhaustion === void 0) {
15011
15068
  return void 0;
15012
15069
  }
15013
15070
  const jobs = dashboardJobs(state);
15014
15071
  switch (state.mode.type) {
15015
15072
  case "table":
15073
+ if (state.jobs.size === 0 && state.discovery.size > 0) {
15074
+ return renderSignupDiscoveryWelcome(state, nowMs);
15075
+ }
15016
15076
  return renderDashboardTable(state, jobs, nowMs);
15017
15077
  case "raw_all":
15018
15078
  return renderRawDashboard(
@@ -15038,9 +15098,195 @@ function renderRunnerConsoleDashboard(state, nowMs) {
15038
15098
  }
15039
15099
  }
15040
15100
  }
15101
+ function renderSignupDiscoveryWelcome(state, nowMs) {
15102
+ const statuses = Array.from(state.discovery.values()).sort(
15103
+ (left, right) => discoveryPhaseSortKey(left.phase).localeCompare(
15104
+ discoveryPhaseSortKey(right.phase)
15105
+ )
15106
+ );
15107
+ const placesSearched = statuses.reduce(
15108
+ (total, status) => total + status.placesSearched,
15109
+ 0
15110
+ );
15111
+ const stillSearching = statuses.some(
15112
+ (status) => ["starting", "running"].includes(status.status)
15113
+ );
15114
+ const statusRows = statuses.length === 0 ? ["Discovery starting 0 places Waiting for discovery agents"] : statuses.map(
15115
+ (status) => [
15116
+ discoveryPhaseLabel(status.phase).padEnd(14),
15117
+ discoveryStatusLabel(status.status).padEnd(10),
15118
+ `${String(status.placesSearched)} places`.padEnd(10),
15119
+ discoveryStatusDetail(status)
15120
+ ].join(" ")
15121
+ );
15122
+ return [
15123
+ runnerWelcomeHeader(),
15124
+ "",
15125
+ "Linzumi setup",
15126
+ "New computer connected",
15127
+ "",
15128
+ `Continue in the browser: ${state.browserUrl ?? "open the Linzumi setup page"}`,
15129
+ "",
15130
+ discoverySummaryText(statuses),
15131
+ `Workspace: ${state.workspace ?? "?"}`,
15132
+ `Runner: ${state.runnerId ?? "?"}`,
15133
+ `Status: ${stillSearching ? "searching" : "finished"} Places searched: ${placesSearched}`,
15134
+ `Last update: ${timeAgo(state.lastUpdateAtMs, nowMs)}`,
15135
+ "",
15136
+ "Phase Status Places Detail",
15137
+ "------------- ---------- --------- ------------------------------",
15138
+ ...statusRows,
15139
+ ""
15140
+ ].join("\n");
15141
+ }
15142
+ function discoveryPhaseSortKey(phase) {
15143
+ switch (phase) {
15144
+ case "projects":
15145
+ return "1-projects";
15146
+ case "conversations":
15147
+ return "2-conversations";
15148
+ default:
15149
+ return `3-${phase}`;
15150
+ }
15151
+ }
15152
+ function discoveryPhaseLabel(phase) {
15153
+ switch (phase) {
15154
+ case "projects":
15155
+ return "Projects";
15156
+ case "conversations":
15157
+ return "Conversations";
15158
+ default:
15159
+ return "Discovery";
15160
+ }
15161
+ }
15162
+ function discoveryStatusLabel(status) {
15163
+ switch (status) {
15164
+ case "starting":
15165
+ return "starting";
15166
+ case "running":
15167
+ return "searching";
15168
+ case "completed":
15169
+ return "done";
15170
+ case "failed":
15171
+ return "failed";
15172
+ default:
15173
+ return status;
15174
+ }
15175
+ }
15176
+ function discoverySummaryText(statuses) {
15177
+ if (statuses.length === 0) {
15178
+ return "I'm waiting for the discovery agents to start.";
15179
+ }
15180
+ const stillSearching = statuses.some(
15181
+ (status) => ["starting", "running"].includes(status.status)
15182
+ );
15183
+ return stillSearching ? "I'm scouting existing projects and coding-agent conversations." : "Discovery is finished and idle. Select a project in the browser or move on.";
15184
+ }
15185
+ function discoveryStatusDetail(status) {
15186
+ if (status.lastError !== void 0) {
15187
+ return status.lastError;
15188
+ }
15189
+ const importProgress = conversationImportProgressText(status);
15190
+ if (importProgress !== void 0) {
15191
+ return importProgress;
15192
+ }
15193
+ const source = discoverySourceLabel(status.currentSource);
15194
+ if (status.currentPlace !== void 0) {
15195
+ return source === void 0 ? status.currentPlace : `${source}: ${status.currentPlace}`;
15196
+ }
15197
+ return status.message ?? discoveryDefaultMessage(status.phase);
15198
+ }
15199
+ function conversationImportProgressText(status) {
15200
+ if (status.phase !== "conversations") {
15201
+ return void 0;
15202
+ }
15203
+ if (!conversationImportMetadata(status.metadata)) {
15204
+ return void 0;
15205
+ }
15206
+ const totalCount = numberValue(status.metadata?.total_count) ?? (numberValue(status.metadata?.codex_count) ?? 0) + (numberValue(status.metadata?.claude_code_count) ?? 0);
15207
+ const reportedCount = numberValue(status.metadata?.imported_count) ?? numberValue(status.metadata?.reported_count);
15208
+ if (reportedCount === void 0 || totalCount <= 0) {
15209
+ return void 0;
15210
+ }
15211
+ const label = status.status === "completed" ? "Imported" : "Importing";
15212
+ return `${label} [${conversationImportProgressBar(
15213
+ reportedCount,
15214
+ totalCount
15215
+ )}] ${reportedCount} / ${totalCount} Codex/Claude Code conversations`;
15216
+ }
15217
+ function conversationImportMetadata(metadata) {
15218
+ return stringValue4(metadata?.worker_id) === "conversations-local-import" || numberValue(metadata?.imported_count) !== void 0 || numberValue(metadata?.failed_import_count) !== void 0 || numberValue(metadata?.total_count) !== void 0;
15219
+ }
15220
+ function conversationImportProgressBar(reportedCount, totalCount) {
15221
+ const width = 10;
15222
+ const filled = Math.min(
15223
+ width,
15224
+ Math.max(0, Math.floor(reportedCount / totalCount * width))
15225
+ );
15226
+ return `${"#".repeat(filled)}${"-".repeat(width - filled)}`;
15227
+ }
15228
+ function mergeConversationImportMetadata(previous, next) {
15229
+ if (previous === void 0) {
15230
+ return next;
15231
+ }
15232
+ if (next === void 0) {
15233
+ return previous;
15234
+ }
15235
+ return {
15236
+ ...previous,
15237
+ ...next,
15238
+ ...maxOptionalMetadataNumber(previous, next, "codex_count"),
15239
+ ...maxOptionalMetadataNumber(previous, next, "claude_code_count"),
15240
+ ...maxOptionalMetadataNumber(previous, next, "reported_count"),
15241
+ ...maxOptionalMetadataNumber(previous, next, "imported_count"),
15242
+ ...maxOptionalMetadataNumber(previous, next, "total_count")
15243
+ };
15244
+ }
15245
+ function maxOptionalMetadataNumber(previous, next, key) {
15246
+ const previousValue = numberValue(previous[key]);
15247
+ const nextValue = numberValue(next[key]);
15248
+ if (previousValue === void 0 && nextValue === void 0) {
15249
+ return {};
15250
+ }
15251
+ return {
15252
+ [key]: Math.max(previousValue ?? 0, nextValue ?? 0)
15253
+ };
15254
+ }
15255
+ function conversationDiscoveryMergedStatus(previous, nextStatus, nextMetadata) {
15256
+ if (previous?.status === "running" && conversationImportMetadata(previous.metadata) && !conversationImportMetadata(nextMetadata)) {
15257
+ return previous.status;
15258
+ }
15259
+ return nextStatus;
15260
+ }
15261
+ function discoverySourceLabel(source) {
15262
+ switch (source) {
15263
+ case "git":
15264
+ return "Git";
15265
+ case "codex":
15266
+ return "Codex";
15267
+ case "claude_code":
15268
+ return "Claude Code";
15269
+ case void 0:
15270
+ return void 0;
15271
+ default:
15272
+ return source;
15273
+ }
15274
+ }
15275
+ function discoveryDefaultMessage(phase) {
15276
+ switch (phase) {
15277
+ case "projects":
15278
+ return "Checking common project folders and Git worktrees.";
15279
+ case "conversations":
15280
+ return "Checking Codex and Claude Code sessions.";
15281
+ default:
15282
+ return "Checking local context.";
15283
+ }
15284
+ }
15041
15285
  function renderDashboardTable(state, jobs, nowMs) {
15042
15286
  const model = dashboardTableModel(state, jobs, nowMs);
15043
15287
  return [
15288
+ runnerWelcomeHeader(),
15289
+ "",
15044
15290
  "Linzumi Commander",
15045
15291
  "",
15046
15292
  `Jobs: ${jobs.length} Last update: ${timeAgo(state.lastUpdateAtMs, nowMs)}`,
@@ -15053,6 +15299,9 @@ function renderDashboardTable(state, jobs, nowMs) {
15053
15299
  ""
15054
15300
  ].join("\n");
15055
15301
  }
15302
+ function runnerWelcomeHeader() {
15303
+ return runnerWelcomeHeaderLines.join("\n");
15304
+ }
15056
15305
  function creditExhaustionBanner(summary) {
15057
15306
  if (summary === void 0) {
15058
15307
  return [];
@@ -15061,7 +15310,12 @@ function creditExhaustionBanner(summary) {
15061
15310
  }
15062
15311
  function dashboardTableModel(state, jobs, nowMs) {
15063
15312
  const selectedJobKey = selectedDashboardJobKey(state, jobs);
15064
- const rows = jobs.length === 0 ? [dashboardTableHeader(), emptyJobRow()] : [
15313
+ const activeImportRows = activeConversationImportRows(state, nowMs);
15314
+ const rows = jobs.length > 0 && activeImportRows.length > 0 ? [
15315
+ dashboardTableHeader(),
15316
+ ...activeImportRows,
15317
+ ...jobs.map((job) => jobRow(job, selectedJobKey, nowMs))
15318
+ ] : jobs.length === 0 && state.discovery.size > 0 ? discoveryTableRows(state, nowMs) : jobs.length === 0 ? [dashboardTableHeader(), emptyJobRow()] : [
15065
15319
  dashboardTableHeader(),
15066
15320
  ...jobs.map((job) => jobRow(job, selectedJobKey, nowMs))
15067
15321
  ];
@@ -15071,6 +15325,91 @@ function dashboardTableModel(state, jobs, nowMs) {
15071
15325
  rows
15072
15326
  };
15073
15327
  }
15328
+ function activeConversationImportRows(state, nowMs) {
15329
+ return Array.from(state.discovery.values()).filter(activeConversationImportStatus).sort((left, right) => right.updatedAtMs - left.updatedAtMs).map((status) => [
15330
+ "",
15331
+ discoveryPhaseLabel(status.phase),
15332
+ discoveryStatusLabel(status.status),
15333
+ discoveryStatusCurrentSearch(status),
15334
+ String(status.placesSearched),
15335
+ discoveryStatusDetail(status),
15336
+ timeAgo(status.updatedAtMs, nowMs),
15337
+ discoveryStatusEventLabel(status),
15338
+ "onboarding.discovery"
15339
+ ]);
15340
+ }
15341
+ function activeConversationImportStatus(status) {
15342
+ return ["starting", "running"].includes(status.status) && conversationImportProgressText(status) !== void 0;
15343
+ }
15344
+ function discoveryTableRows(state, nowMs) {
15345
+ const statuses = Array.from(state.discovery.values()).sort(
15346
+ (left, right) => discoveryPhaseSortKey(left.phase).localeCompare(
15347
+ discoveryPhaseSortKey(right.phase)
15348
+ )
15349
+ );
15350
+ return [
15351
+ [
15352
+ "",
15353
+ "Onboarding scout",
15354
+ "Status",
15355
+ "Current search",
15356
+ "Checked",
15357
+ "Detail",
15358
+ "Ago",
15359
+ "Latest event",
15360
+ "Event type"
15361
+ ],
15362
+ ...statuses.length === 0 ? [
15363
+ [
15364
+ "",
15365
+ "Discovery",
15366
+ "starting",
15367
+ "Waiting for agents",
15368
+ "0",
15369
+ "Waiting for Commander discovery agents",
15370
+ "?",
15371
+ "onboarding",
15372
+ "discovery"
15373
+ ]
15374
+ ] : statuses.map((status) => [
15375
+ "",
15376
+ discoveryPhaseLabel(status.phase),
15377
+ discoveryStatusLabel(status.status),
15378
+ discoveryStatusCurrentSearch(status),
15379
+ String(status.placesSearched),
15380
+ discoveryStatusDetail(status),
15381
+ timeAgo(status.updatedAtMs, nowMs),
15382
+ discoveryStatusEventLabel(status),
15383
+ "onboarding.discovery"
15384
+ ])
15385
+ ];
15386
+ }
15387
+ function discoveryStatusEventLabel(status) {
15388
+ if (["starting", "running"].includes(status.status) && conversationImportProgressText(status) !== void 0) {
15389
+ return "importing";
15390
+ }
15391
+ switch (status.status) {
15392
+ case "completed":
15393
+ return "idle";
15394
+ case "failed":
15395
+ return "stopped";
15396
+ default:
15397
+ return "scouting";
15398
+ }
15399
+ }
15400
+ function discoveryStatusCurrentSearch(status) {
15401
+ if (status.lastError !== void 0) {
15402
+ return "stopped";
15403
+ }
15404
+ switch (status.status) {
15405
+ case "completed":
15406
+ return "finished";
15407
+ case "failed":
15408
+ return "stopped";
15409
+ default:
15410
+ return status.currentPlace ?? status.message ?? discoveryDefaultMessage(status.phase);
15411
+ }
15412
+ }
15074
15413
  function dashboardTableHeader() {
15075
15414
  return [
15076
15415
  "",
@@ -15115,6 +15454,10 @@ function formatRunnerConsoleEvent(event, payload) {
15115
15454
  return ignoredMessage(payload);
15116
15455
  case "kandan.message_queued":
15117
15456
  return `Incoming message from ${sender(payload)}: queued seq=${text(payload.seq)} depth=${text(payload.queue_depth)}`;
15457
+ case "onboarding.discovery_status":
15458
+ return onboardingDiscoveryStatusLine(payload);
15459
+ case "onboarding.discovery_initial_search_completed":
15460
+ return onboardingDiscoveryInitialSearchCompletedLine(payload);
15118
15461
  case "kandan.chat_event_failed":
15119
15462
  return `Incoming message handling failed: seq=${text(payload.seq)} reason=${text(payload.message)}`;
15120
15463
  case "kandan.reconnected":
@@ -15154,6 +15497,20 @@ function formatRunnerConsoleEvent(event, payload) {
15154
15497
  return void 0;
15155
15498
  }
15156
15499
  }
15500
+ function onboardingDiscoveryStatusLine(payload) {
15501
+ const phase = discoveryPhaseLabel(stringValue4(payload.phase) ?? "discovery");
15502
+ const status = discoveryStatusLabel(stringValue4(payload.status) ?? "unknown");
15503
+ const placesSearched = numberValue(payload.places_searched) ?? 0;
15504
+ const currentPlace = stringValue4(payload.current_place);
15505
+ const message = stringValue4(payload.message);
15506
+ const detail = stringValue4(payload.last_error) ?? currentPlace ?? message ?? "Checking local context";
15507
+ return `Onboarding discovery: ${phase} ${status} checked=${placesSearched} current=${detail}`;
15508
+ }
15509
+ function onboardingDiscoveryInitialSearchCompletedLine(payload) {
15510
+ const durationMs = numberValue(payload.duration_ms) ?? 0;
15511
+ const seconds = (durationMs / 1e3).toFixed(1);
15512
+ return `Onboarding discovery initial search completed in ${durationMs}ms (${seconds}s) workspace=${text(payload.workspace)} runner=${text(payload.runnerId)}`;
15513
+ }
15157
15514
  function connectedRunnerMessage(payload) {
15158
15515
  return [
15159
15516
  "Connected to Linzumi",
@@ -15441,11 +15798,11 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
15441
15798
  top: 0,
15442
15799
  left: 0,
15443
15800
  width: "100%",
15444
- height: 6,
15801
+ height: 1,
15445
15802
  tags: false
15446
15803
  });
15447
15804
  const tableElement = blessed.listtable({
15448
- top: 6,
15805
+ top: 1,
15449
15806
  left: 0,
15450
15807
  width: "100%",
15451
15808
  bottom: 1,
@@ -15567,7 +15924,15 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
15567
15924
  switch (state.mode.type) {
15568
15925
  case "table": {
15569
15926
  const model = dashboardTableModel(state, dashboardJobs(state), nowMs);
15570
- header.setContent(tuiHeaderContent(state, model.jobs.length, nowMs));
15927
+ const headerLayout = runnerConsoleTuiHeaderLayout(
15928
+ state,
15929
+ model.jobs.length,
15930
+ nowMs,
15931
+ tuiScreenHeight(screen)
15932
+ );
15933
+ header.height = headerLayout.height;
15934
+ tableElement.top = headerLayout.height;
15935
+ header.setContent(headerLayout.content);
15571
15936
  header.show();
15572
15937
  tableElement.setData(model.rows);
15573
15938
  tableElement.show();
@@ -15618,6 +15983,9 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
15618
15983
  }
15619
15984
  return { render, destroy };
15620
15985
  }
15986
+ function tuiScreenHeight(screen) {
15987
+ return typeof screen.height === "number" ? screen.height : 24;
15988
+ }
15621
15989
  function syncTuiTableSelection(tableElement, model) {
15622
15990
  const selectedIndex = model.jobs.findIndex(
15623
15991
  (job) => job.key === model.selectedJobKey
@@ -15625,8 +15993,46 @@ function syncTuiTableSelection(tableElement, model) {
15625
15993
  const tableIndex = selectedIndex < 0 ? 1 : selectedIndex + 1;
15626
15994
  tableElement.select(tableIndex);
15627
15995
  }
15628
- function tuiHeaderContent(state, jobCount, nowMs) {
15996
+ function runnerConsoleTuiHeaderLayout(state, jobCount, nowMs, terminalRows) {
15997
+ const fullContent = tuiHeaderContent(
15998
+ state,
15999
+ jobCount,
16000
+ nowMs,
16001
+ runnerWelcomeHeader()
16002
+ );
16003
+ const fullHeight = tuiHeaderHeight(fullContent);
16004
+ if (terminalRows - fullHeight >= minimumVisibleTuiTableRows) {
16005
+ return { content: fullContent, height: fullHeight };
16006
+ }
16007
+ const compactContent = tuiHeaderContent(
16008
+ state,
16009
+ jobCount,
16010
+ nowMs,
16011
+ runnerWelcomeCompactHeader()
16012
+ );
16013
+ return {
16014
+ content: compactContent,
16015
+ height: tuiHeaderHeight(compactContent)
16016
+ };
16017
+ }
16018
+ function tuiHeaderHeight(content) {
16019
+ return content.split("\n").length + 1;
16020
+ }
16021
+ function tuiHeaderContent(state, jobCount, nowMs, welcomeHeader) {
16022
+ if (jobCount === 0 && state.discovery.size > 0) {
16023
+ return [
16024
+ welcomeHeader,
16025
+ "",
16026
+ "Linzumi Commander",
16027
+ discoveryHeaderStatus(state),
16028
+ `Workspace: ${state.workspace ?? "?"} Runner: ${state.runnerId ?? "?"}`,
16029
+ `Last update: ${timeAgo(state.lastUpdateAtMs, nowMs)}`,
16030
+ "Controls: r raw stream | esc/back table | mouse wheel scroll"
16031
+ ].join("\n");
16032
+ }
15629
16033
  return [
16034
+ welcomeHeader,
16035
+ "",
15630
16036
  "Linzumi Commander",
15631
16037
  `Jobs: ${jobCount} Last update: ${timeAgo(state.lastUpdateAtMs, nowMs)}`,
15632
16038
  ...creditExhaustionBanner(state.creditExhaustion),
@@ -15635,6 +16041,20 @@ function tuiHeaderContent(state, jobCount, nowMs) {
15635
16041
  "Controls: click row/enter raw job | r raw stream | esc/back table | mouse wheel scroll"
15636
16042
  ].join("\n");
15637
16043
  }
16044
+ function runnerWelcomeCompactHeader() {
16045
+ return runnerWelcomeCompactHeaderLines.join("\n");
16046
+ }
16047
+ function discoveryHeaderStatus(state) {
16048
+ const statuses = Array.from(state.discovery.values());
16049
+ const placesSearched = statuses.reduce(
16050
+ (total, status) => total + status.placesSearched,
16051
+ 0
16052
+ );
16053
+ const stillSearching = statuses.some(
16054
+ (status) => ["starting", "running"].includes(status.status)
16055
+ );
16056
+ return `Onboarding discovery: ${stillSearching ? "searching" : "finished/idle"} Places searched: ${placesSearched}`;
16057
+ }
15638
16058
  function updateRunnerConsoleDashboardMode(state, key) {
15639
16059
  switch (key) {
15640
16060
  case "r":
@@ -15888,12 +16308,13 @@ function stringValue4(value) {
15888
16308
  function numberValue(value) {
15889
16309
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
15890
16310
  }
15891
- var dashboardState, maxRawLines, escapeKey, ctrlCKey, enterKey, upKey, downKey, dashboardTableColumns, redrawScreen, keyboardState, tickerState, tuiState;
16311
+ var dashboardState, maxRawLines, escapeKey, ctrlCKey, enterKey, upKey, downKey, dashboardTableColumns, runnerWelcomeHeaderLines, runnerWelcomeCompactHeaderLines, minimumVisibleTuiTableRows, redrawScreen, keyboardState, tickerState, tuiState;
15892
16312
  var init_runnerConsoleReporter = __esm({
15893
16313
  "src/runnerConsoleReporter.ts"() {
15894
16314
  "use strict";
15895
16315
  dashboardState = {
15896
16316
  jobs: /* @__PURE__ */ new Map(),
16317
+ discovery: /* @__PURE__ */ new Map(),
15897
16318
  rawLines: [],
15898
16319
  nextJobOrdinal: 0,
15899
16320
  mode: { type: "table" },
@@ -15901,7 +16322,10 @@ var init_runnerConsoleReporter = __esm({
15901
16322
  tokenUsage: void 0,
15902
16323
  rateLimit: void 0,
15903
16324
  creditExhaustion: void 0,
15904
- lastUpdateAtMs: void 0
16325
+ lastUpdateAtMs: void 0,
16326
+ browserUrl: void 0,
16327
+ workspace: void 0,
16328
+ runnerId: void 0
15905
16329
  };
15906
16330
  maxRawLines = 500;
15907
16331
  escapeKey = "\x1B";
@@ -15920,6 +16344,37 @@ var init_runnerConsoleReporter = __esm({
15920
16344
  { width: 12 },
15921
16345
  { width: 24 }
15922
16346
  ];
16347
+ runnerWelcomeHeaderLines = [
16348
+ "\u2593\u2593\u2557 \u2593\u2593\u2557\u2593\u2593\u2593\u2557 \u2593\u2593\u2557\u2593\u2593\u2593\u2593\u2593\u2593\u2593\u2557\u2593\u2593\u2557 \u2593\u2593\u2557\u2593\u2593\u2593\u2557 \u2593\u2593\u2593\u2557\u2593\u2593\u2557",
16349
+ "\u2593\u2593\u2551 \u2593\u2593\u2551\u2593\u2593\u2593\u2593\u2557 \u2593\u2593\u2551\u255A\u2550\u2550\u2593\u2593\u2593\u2554\u255D\u2593\u2593\u2551 \u2593\u2593\u2551\u2593\u2593\u2593\u2593\u2557 \u2593\u2593\u2593\u2593\u2551\u2593\u2593\u2551",
16350
+ "\u2593\u2593\u2551 \u2593\u2593\u2551\u2593\u2593\u2554\u2593\u2593\u2557 \u2593\u2593\u2551 \u2593\u2593\u2593\u2554\u255D \u2593\u2593\u2551 \u2593\u2593\u2551\u2593\u2593\u2554\u2593\u2593\u2593\u2593\u2554\u2593\u2593\u2551\u2593\u2593\u2551",
16351
+ "\u2593\u2593\u2551 \u2593\u2593\u2551\u2593\u2593\u2551\u255A\u2593\u2593\u2557\u2593\u2593\u2551 \u2593\u2593\u2593\u2554\u255D \u2593\u2593\u2551 \u2593\u2593\u2551\u2593\u2593\u2551\u255A\u2593\u2593\u2554\u255D\u2593\u2593\u2551\u2593\u2593\u2551",
16352
+ "\u2593\u2593\u2593\u2593\u2593\u2593\u2593\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u255A\u2593\u2593\u2593\u2593\u2551\u2593\u2593\u2593\u2593\u2593\u2593\u2593\u2557\u255A\u2593\u2593\u2593\u2593\u2593\u2593\u2554\u255D\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2593\u2593\u2551\u2593\u2593\u2551",
16353
+ "\u255A\u2550\u2550\u2550\u2592\u2592\u2592@@@@@@@@\u2592\u2592\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2592\u2592\u2592@@@@@@@@\u2592\u2592\u255D\u255A\u2550\u255D",
16354
+ " \u2592@@@@@@@@@@@@@@@@\u2592 \u2592@@@@@@@@@@@@@@@@\u2592",
16355
+ "@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@",
16356
+ " @@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@",
16357
+ " \u2551\u2551 \u2551\u2551",
16358
+ " \u2551\u2551 \u2551\u2551",
16359
+ " \u2551\u2551 ()-().----. \u2551\u2551",
16360
+ " \u2551\u2551 \"/` ___ ;_________\u2551\u2551_.'",
16361
+ " \u2551\u2551 ` ^^ ^^ \u2551\u2551",
16362
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2568\u2568\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\xB7\xB7\u2500\u2500\u2500\u2500\xB7\xB7\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2568\u2568\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
16363
+ "Welcome to the Linzumi runner.",
16364
+ "Codex is running on this computer and connected to Linzumi.",
16365
+ "Active Codex jobs appear below as they run.",
16366
+ "You can return to the Linzumi app now."
16367
+ ];
16368
+ runnerWelcomeCompactHeaderLines = [
16369
+ "\u2593\u2593\u2557 LINZUMI RUNNER \u2593\u2593\u2557",
16370
+ "\u2551\u2551 ()-().----. \u2551\u2551",
16371
+ "\u2568\u2568\u2500\u2500 connected to Codex and Linzumi \u2500\u2500\u2568\u2568",
16372
+ "Welcome to the Linzumi runner.",
16373
+ "Codex is running on this computer and connected to Linzumi.",
16374
+ "Active Codex jobs appear below as they run.",
16375
+ "You can return to the Linzumi app now."
16376
+ ];
16377
+ minimumVisibleTuiTableRows = 6;
15923
16378
  redrawScreen = (screen) => {
15924
16379
  process.stdout.write(`\x1B[2J\x1B[H${screen}`);
15925
16380
  };
@@ -15932,19 +16387,1052 @@ var init_runnerConsoleReporter = __esm({
15932
16387
  }
15933
16388
  });
15934
16389
 
15935
- // src/authCache.ts
15936
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
16390
+ // src/telemetry.ts
16391
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
16392
+ import { dirname as dirname8, basename as basename5, join as join12 } from "node:path";
16393
+ import { randomUUID as randomUUID3 } from "node:crypto";
16394
+ function peekFlagValue(args, flags) {
16395
+ for (let index = 0; index < args.length; index += 1) {
16396
+ const arg = args[index] ?? "";
16397
+ for (const flag of flags) {
16398
+ if (arg.startsWith(`${flag}=`)) {
16399
+ return arg.slice(flag.length + 1);
16400
+ }
16401
+ }
16402
+ const value = args[index + 1];
16403
+ if (flags.includes(arg) && value !== void 0 && !value.startsWith("--")) {
16404
+ return value;
16405
+ }
16406
+ }
16407
+ return void 0;
16408
+ }
16409
+ function telemetryDisabled(env = process.env) {
16410
+ const optOut = env.LINZUMI_TELEMETRY?.trim().toLowerCase();
16411
+ if (optOut === "0" || optOut === "false") {
16412
+ return true;
16413
+ }
16414
+ return env.NODE_ENV === "test" || env.VITEST !== void 0;
16415
+ }
16416
+ function telemetryBaseUrl(apiUrl, env = process.env) {
16417
+ const override = env.LINZUMI_TELEMETRY_BASE_URL?.trim();
16418
+ if (override !== void 0 && override !== "") {
16419
+ return override.replace(/\/+$/, "");
16420
+ }
16421
+ if (apiUrl === void 0 || apiUrl.trim() === "") {
16422
+ return defaultLinzumiHttpUrl;
16423
+ }
16424
+ try {
16425
+ return kandanHttpBaseUrl(apiUrl);
16426
+ } catch {
16427
+ return defaultLinzumiHttpUrl;
16428
+ }
16429
+ }
16430
+ function telemetryInstallIdPath(env = process.env) {
16431
+ return join12(dirname8(localConfigPath(env)), "install-id");
16432
+ }
16433
+ function ensureTelemetryInstallId(env = process.env) {
16434
+ const path2 = telemetryInstallIdPath(env);
16435
+ try {
16436
+ const existing = readFileSync9(path2, "utf8").trim();
16437
+ if (existing !== "" && existing.length <= 128) {
16438
+ return existing;
16439
+ }
16440
+ } catch {
16441
+ }
16442
+ const installId = randomUUID3();
16443
+ try {
16444
+ mkdirSync8(dirname8(path2), { recursive: true });
16445
+ writeFileSync6(path2, `${installId}
16446
+ `, { mode: 384 });
16447
+ } catch {
16448
+ }
16449
+ return installId;
16450
+ }
16451
+ function buildLifecyclePayload(event, context) {
16452
+ const severity = event.severity ?? "INFO";
16453
+ return {
16454
+ resourceLogs: [
16455
+ {
16456
+ resource: {
16457
+ attributes: otlpAttributes({
16458
+ "service.name": "linzumi-cli",
16459
+ "service.version": linzumiCliVersion,
16460
+ "linzumi.install_id": context.installId,
16461
+ "os.platform": context.platform ?? process.platform,
16462
+ "host.arch": context.arch ?? process.arch
16463
+ })
16464
+ },
16465
+ scopeLogs: [
16466
+ {
16467
+ scope: { name: "linzumi-cli" },
16468
+ logRecords: [
16469
+ {
16470
+ timeUnixNano: context.timeUnixNano ?? `${BigInt(Date.now()) * 1000000n}`,
16471
+ severityNumber: severity === "ERROR" ? 17 : 9,
16472
+ severityText: severity,
16473
+ body: { stringValue: event.name },
16474
+ attributes: otlpAttributes(event.attributes ?? {})
16475
+ }
16476
+ ]
16477
+ }
16478
+ ]
16479
+ }
16480
+ ]
16481
+ };
16482
+ }
16483
+ function otlpAttributes(attributes) {
16484
+ return Object.entries(attributes).map(([key, value]) => ({
16485
+ key,
16486
+ value: otlpAttributeValue(value)
16487
+ }));
16488
+ }
16489
+ function otlpAttributeValue(value) {
16490
+ switch (typeof value) {
16491
+ case "string":
16492
+ return { stringValue: value };
16493
+ case "boolean":
16494
+ return { boolValue: value };
16495
+ default:
16496
+ return Number.isSafeInteger(value) ? { intValue: `${value}` } : { doubleValue: value };
16497
+ }
16498
+ }
16499
+ function lifecycleErrorAttributes(error) {
16500
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
16501
+ const rawMessage = error instanceof Error ? error.message : String(error);
16502
+ const withoutPaths = rawMessage.replace(
16503
+ /(?:[A-Za-z]:)?(?:[\\/][^\s\\/:"']+){2,}/g,
16504
+ (path2) => basename5(path2)
16505
+ );
16506
+ const message = withoutPaths.length > maxErrorMessageLength ? `${withoutPaths.slice(0, maxErrorMessageLength)}\u2026` : withoutPaths;
16507
+ return { error_class: errorClass, error_message: message };
16508
+ }
16509
+ function postLifecycleEvent(event, opts = {}) {
16510
+ try {
16511
+ const env = opts.env ?? process.env;
16512
+ if (telemetryDisabled(env)) {
16513
+ return Promise.resolve();
16514
+ }
16515
+ const payload = buildLifecyclePayload(event, {
16516
+ installId: ensureTelemetryInstallId(env)
16517
+ });
16518
+ const fetchImpl = opts.fetchImpl ?? fetch;
16519
+ return Promise.resolve().then(
16520
+ () => fetchImpl(
16521
+ `${telemetryBaseUrl(opts.apiUrl, env)}${telemetryEndpointPath}`,
16522
+ {
16523
+ method: "POST",
16524
+ headers: { "content-type": "application/json" },
16525
+ body: JSON.stringify(payload),
16526
+ // AbortSignal.timeout timers are unref'd by Node, so a pending
16527
+ // telemetry request never keeps a short-lived process alive past
16528
+ // the bounded window and never delays process exit.
16529
+ signal: AbortSignal.timeout(
16530
+ opts.timeoutMs ?? defaultTelemetryTimeoutMs
16531
+ )
16532
+ }
16533
+ )
16534
+ ).then((response) => {
16535
+ void response.arrayBuffer().catch(() => void 0);
16536
+ }).catch(() => void 0);
16537
+ } catch {
16538
+ return Promise.resolve();
16539
+ }
16540
+ }
16541
+ var telemetryEndpointPath, defaultTelemetryTimeoutMs, maxErrorMessageLength;
16542
+ var init_telemetry = __esm({
16543
+ "src/telemetry.ts"() {
16544
+ "use strict";
16545
+ init_localConfig();
16546
+ init_oauth();
16547
+ init_defaultUrls();
16548
+ init_version();
16549
+ telemetryEndpointPath = "/api/v1/observability/logs";
16550
+ defaultTelemetryTimeoutMs = 2e3;
16551
+ maxErrorMessageLength = 200;
16552
+ }
16553
+ });
16554
+
16555
+ // src/linzumiApiClient.ts
16556
+ function createLinzumiMcpApiClient(options) {
16557
+ const fetchImpl = options.fetchImpl ?? fetch;
16558
+ const baseUrl = kandanHttpBaseUrl(options.kandanUrl);
16559
+ const apiPrefix = options.authMode === "personal-agent-delegation" ? "/api/v2/personal-agent-mcp" : "/api/v2/local-runner-mcp";
16560
+ const operatingMode = options.operatingMode ?? "text";
16561
+ const request = async (method, path2, params) => {
16562
+ const url = new URL(path2, baseUrl);
16563
+ const paramsWithMode = {
16564
+ ...params,
16565
+ operating_mode: operatingMode
16566
+ };
16567
+ const requestInit = {
16568
+ method,
16569
+ headers: { authorization: `Bearer ${options.accessToken}` }
16570
+ };
16571
+ if (method === "GET") {
16572
+ for (const [key, value] of Object.entries(paramsWithMode)) {
16573
+ if (value !== void 0 && value !== null) {
16574
+ url.searchParams.set(key, String(value));
16575
+ }
16576
+ }
16577
+ } else {
16578
+ requestInit.headers = {
16579
+ ...requestInit.headers,
16580
+ "content-type": "application/json"
16581
+ };
16582
+ requestInit.body = JSON.stringify(paramsWithMode);
16583
+ }
16584
+ const response = await fetchImpl(url, requestInit);
16585
+ const parsed = await response.json();
16586
+ const body = isJsonObject(parsed) ? parsed : void 0;
16587
+ if (body === void 0) {
16588
+ throw new Error(`Linzumi MCP API returned non-object JSON from ${path2}`);
16589
+ }
16590
+ if (response.ok && body.ok === true) {
16591
+ return body;
16592
+ }
16593
+ const error = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
16594
+ throw new Error(`Linzumi MCP API ${path2} failed: ${error}`);
16595
+ };
16596
+ return {
16597
+ validateAuth: () => request("GET", `${apiPrefix}/validate`, {}),
16598
+ issueOnboardingDiscoveryToken: (params) => request("POST", `${apiPrefix}/onboarding-discovery-token`, params),
16599
+ getMessage: (params) => request("GET", `${apiPrefix}/message`, params),
16600
+ getThread: (params) => request("GET", `${apiPrefix}/thread`, params),
16601
+ getChannel: (params) => request("GET", `${apiPrefix}/channel`, params),
16602
+ getCodingJobMetadata: (params) => request("GET", `${apiPrefix}/coding-job-metadata`, params),
16603
+ renameCodingJob: (params) => request("POST", `${apiPrefix}/coding-job-title`, params),
16604
+ upsertCodingJobPlan: (params) => request("POST", `${apiPrefix}/coding-job-plan`, params),
16605
+ replaceCodingJobPlanSteps: (params) => request("POST", `${apiPrefix}/coding-job-plan/steps`, params),
16606
+ updateCodingJobPlanStep: (params) => request("POST", `${apiPrefix}/coding-job-plan/step`, params),
16607
+ linkCodingJobPullRequest: (params) => request("POST", `${apiPrefix}/coding-job-primary-pr`, params),
16608
+ listVaultSecrets: (params) => request("GET", `${apiPrefix}/vault-secrets`, params),
16609
+ noteProjectDirectory: (params) => request("POST", `${apiPrefix}/discovered-project`, params),
16610
+ noteAgentConversation: (params) => request("POST", `${apiPrefix}/discovered-conversation`, params),
16611
+ noteOnboardingDiscoveryStatus: (params) => request("POST", `${apiPrefix}/discovery-status`, params),
16612
+ sendChannelMessage: (params) => request("POST", `${apiPrefix}/channel-message`, params),
16613
+ prepareMessageUploads: (params) => request("POST", `${apiPrefix}/message-uploads/prepare`, params),
16614
+ attachMessageFiles: (params) => request("POST", `${apiPrefix}/message-files/attach`, params),
16615
+ prepareCustomEmoji: (params) => request("POST", `${apiPrefix}/custom-emoji/prepare`, params),
16616
+ renameCustomEmoji: (params) => request("POST", `${apiPrefix}/custom-emoji/rename`, params),
16617
+ sendThreadReply: (params) => request("POST", `${apiPrefix}/thread-reply`, params),
16618
+ sendDm: (params) => request("POST", `${apiPrefix}/dm`, params),
16619
+ dmOwner: (params) => request("POST", `${apiPrefix}/dm-owner`, params),
16620
+ getVaultValues: (params) => request("POST", `${apiPrefix}/vault-values`, params)
16621
+ };
16622
+ }
16623
+ var init_linzumiApiClient = __esm({
16624
+ "src/linzumiApiClient.ts"() {
16625
+ "use strict";
16626
+ init_oauth();
16627
+ init_protocol();
16628
+ }
16629
+ });
16630
+
16631
+ // src/onboardingConversationDiscovery.ts
16632
+ import {
16633
+ closeSync as closeSync2,
16634
+ existsSync as existsSync8,
16635
+ openSync as openSync3,
16636
+ readdirSync as readdirSync2,
16637
+ readFileSync as readFileSync10,
16638
+ readSync,
16639
+ statSync
16640
+ } from "node:fs";
16641
+ import { basename as basename6, join as join13 } from "node:path";
15937
16642
  import { homedir as homedir9 } from "node:os";
15938
- import { dirname as dirname8, join as join12 } from "node:path";
16643
+ function discoverLocalConversations(options = {}) {
16644
+ const startedAtMs = options.nowMs ?? Date.now();
16645
+ const homeDir = options.homeDir ?? homedir9();
16646
+ const env = options.env ?? process.env;
16647
+ const codexRoot = join13(env.CODEX_HOME ?? join13(homeDir, ".codex"), "sessions");
16648
+ const claudeRoot = join13(
16649
+ env.CLAUDE_CONFIG_DIR ?? join13(homeDir, ".claude"),
16650
+ "projects"
16651
+ );
16652
+ const codexStartedAtMs = Date.now();
16653
+ const codexFiles = listJsonlFiles(codexRoot);
16654
+ const codexDurationMs = Date.now() - codexStartedAtMs;
16655
+ const claudeStartedAtMs = Date.now();
16656
+ const claudeFiles = listJsonlFiles(claudeRoot);
16657
+ const claudeDurationMs = Date.now() - claudeStartedAtMs;
16658
+ const claudeTopLevelFiles = claudeFiles.filter(
16659
+ (filePath) => !filePath.replaceAll("\\", "/").includes("/subagents/")
16660
+ );
16661
+ const candidateLimit = normalizeCandidateLimit(options.candidateLimit);
16662
+ const candidateRecords = [
16663
+ ...conversationFileRecords("codex", codexFiles),
16664
+ ...conversationFileRecords("claude_code", claudeTopLevelFiles)
16665
+ ].sort(compareConversationFileRecords).slice(0, candidateLimit);
16666
+ const candidates = candidateRecords.flatMap((record) => {
16667
+ const candidate = candidateFromRecord(record);
16668
+ return candidate === void 0 ? [] : [candidate];
16669
+ });
16670
+ return {
16671
+ candidates,
16672
+ codexCount: codexFiles.length,
16673
+ claudeCodeCount: claudeTopLevelFiles.length,
16674
+ claudeCodeSubagentCount: claudeFiles.length - claudeTopLevelFiles.length,
16675
+ placesSearched: existingPathCount([codexRoot, claudeRoot]),
16676
+ durationMs: Math.max(0, Date.now() - startedAtMs),
16677
+ searchStats: [
16678
+ {
16679
+ place: codexRoot,
16680
+ source: "codex",
16681
+ duration_ms: codexDurationMs,
16682
+ result_count: codexFiles.length
16683
+ },
16684
+ {
16685
+ place: claudeRoot,
16686
+ source: "claude_code",
16687
+ duration_ms: claudeDurationMs,
16688
+ result_count: claudeTopLevelFiles.length,
16689
+ subagent_result_count: claudeFiles.length - claudeTopLevelFiles.length
16690
+ }
16691
+ ]
16692
+ };
16693
+ }
16694
+ function readLocalConversationImportMessages(candidate) {
16695
+ const lines = jsonLines(candidate.path);
16696
+ switch (candidate.source) {
16697
+ case "codex":
16698
+ return codexImportMessages(lines);
16699
+ case "claude_code":
16700
+ return claudeImportMessages(lines);
16701
+ }
16702
+ }
16703
+ function normalizeCandidateLimit(value) {
16704
+ if (value === void 0) {
16705
+ return defaultCandidateLimit;
16706
+ }
16707
+ if (Number.isFinite(value) && value > 0) {
16708
+ return Math.floor(value);
16709
+ }
16710
+ return defaultCandidateLimit;
16711
+ }
16712
+ function conversationFileRecords(source, paths) {
16713
+ return paths.flatMap((path2) => {
16714
+ try {
16715
+ return [
16716
+ {
16717
+ source,
16718
+ path: path2,
16719
+ mtimeMs: statSync(path2).mtimeMs
16720
+ }
16721
+ ];
16722
+ } catch (_error) {
16723
+ return [];
16724
+ }
16725
+ });
16726
+ }
16727
+ function compareConversationFileRecords(left, right) {
16728
+ return right.mtimeMs - left.mtimeMs || left.path.localeCompare(right.path);
16729
+ }
16730
+ function candidateFromRecord(record) {
16731
+ switch (record.source) {
16732
+ case "codex":
16733
+ return codexCandidateFromPath(
16734
+ record.path,
16735
+ new Date(record.mtimeMs).toISOString()
16736
+ );
16737
+ case "claude_code":
16738
+ return claudeCandidateFromPath(
16739
+ record.path,
16740
+ new Date(record.mtimeMs).toISOString()
16741
+ );
16742
+ }
16743
+ }
16744
+ function listJsonlFiles(root) {
16745
+ if (!existsSync8(root)) {
16746
+ return [];
16747
+ }
16748
+ const walk = (directory) => {
16749
+ let entries;
16750
+ try {
16751
+ entries = readdirSync2(directory, { withFileTypes: true });
16752
+ } catch (_error) {
16753
+ return [];
16754
+ }
16755
+ return entries.flatMap((entry) => {
16756
+ const filePath = join13(directory, entry.name);
16757
+ if (entry.isDirectory()) {
16758
+ return walk(filePath);
16759
+ }
16760
+ return entry.isFile() && entry.name.endsWith(".jsonl") ? [filePath] : [];
16761
+ });
16762
+ };
16763
+ return walk(root);
16764
+ }
16765
+ function existingPathCount(paths) {
16766
+ return paths.filter((path2) => existsSync8(path2)).length;
16767
+ }
16768
+ function codexCandidateFromPath(path2, activityAt) {
16769
+ const lines = boundedJsonLines(path2);
16770
+ const sessionMeta = objectValue(
16771
+ lines.find((line) => line.type === "session_meta")?.payload
16772
+ );
16773
+ const sessionId = stringValue(sessionMeta?.id) ?? sessionIdFromFilename(path2);
16774
+ const cwd = stringValue(sessionMeta?.cwd);
16775
+ const firstUserMessage = firstCodexUserMessage(lines);
16776
+ if (firstUserMessage === void 0 && hasInternalCodexDiscoveryPrompt(lines)) {
16777
+ return void 0;
16778
+ }
16779
+ const displayName = cleanConversationTitle(firstUserMessage);
16780
+ if (displayName === void 0) {
16781
+ return void 0;
16782
+ }
16783
+ return {
16784
+ source: "codex",
16785
+ importKey: sessionId ?? path2,
16786
+ path: path2,
16787
+ displayName,
16788
+ activityAt,
16789
+ metadata: compactJsonObject({
16790
+ session_id: sessionId,
16791
+ project_path: cwd,
16792
+ title: displayName,
16793
+ first_user_message: cleanConversationTitle(firstUserMessage),
16794
+ discovery_method: "local_filesystem"
16795
+ })
16796
+ };
16797
+ }
16798
+ function claudeCandidateFromPath(path2, activityAt) {
16799
+ const lines = boundedJsonLines(path2);
16800
+ const sessionId = lines.map((line) => stringValue(line.sessionId)).find(Boolean) ?? basename6(path2, ".jsonl");
16801
+ const cwd = lines.map((line) => stringValue(line.cwd)).find(Boolean);
16802
+ const aiTitle = lines.map((line) => stringValue(line.aiTitle)).find(Boolean);
16803
+ const firstUserMessage = firstClaudeUserMessage(lines);
16804
+ const displayName = cleanConversationTitle(aiTitle) ?? cleanConversationTitle(firstUserMessage);
16805
+ if (displayName === void 0) {
16806
+ return void 0;
16807
+ }
16808
+ return {
16809
+ source: "claude_code",
16810
+ importKey: sessionId,
16811
+ path: path2,
16812
+ displayName,
16813
+ activityAt,
16814
+ metadata: compactJsonObject({
16815
+ session_id: sessionId,
16816
+ project_path: cwd,
16817
+ title: displayName,
16818
+ first_user_message: cleanConversationTitle(firstUserMessage),
16819
+ discovery_method: "local_filesystem"
16820
+ })
16821
+ };
16822
+ }
16823
+ function boundedJsonLines(path2) {
16824
+ return boundedTranscriptText(path2).flatMap((line) => {
16825
+ try {
16826
+ const parsed = JSON.parse(line);
16827
+ return isJsonObject(parsed) ? [parsed] : [];
16828
+ } catch (_error) {
16829
+ return [];
16830
+ }
16831
+ });
16832
+ }
16833
+ function jsonLines(path2) {
16834
+ try {
16835
+ return readFileSync10(path2, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
16836
+ try {
16837
+ const parsed = JSON.parse(line);
16838
+ return isJsonObject(parsed) ? [parsed] : [];
16839
+ } catch (_error) {
16840
+ return [];
16841
+ }
16842
+ });
16843
+ } catch (_error) {
16844
+ return [];
16845
+ }
16846
+ }
16847
+ function codexImportMessages(lines) {
16848
+ return lines.flatMap((line, index) => {
16849
+ if (line.type !== "response_item") {
16850
+ return [];
16851
+ }
16852
+ const payload = objectValue(line.payload);
16853
+ const role = importRole(stringValue(payload?.role));
16854
+ const content = payload?.content;
16855
+ const body = importMessageBody(textFromContent(content), role);
16856
+ const attachments = attachmentsFromContent(content);
16857
+ const activityAt = importMessageActivityAt(line, payload);
16858
+ if (body === void 0 && attachments.length === 0) {
16859
+ return [];
16860
+ }
16861
+ return [
16862
+ {
16863
+ itemKey: `codex:${index}`,
16864
+ role,
16865
+ body: body ?? attachmentOnlyBody(role, attachments.length),
16866
+ attachments,
16867
+ activityAt
16868
+ }
16869
+ ];
16870
+ });
16871
+ }
16872
+ function claudeImportMessages(lines) {
16873
+ return lines.flatMap((line, index) => {
16874
+ const message = objectValue(line.message);
16875
+ const role = importRole(
16876
+ stringValue(message?.role) ?? stringValue(line.type)
16877
+ );
16878
+ const content = message?.content ?? line.content;
16879
+ const body = importMessageBody(textFromContent(content), role);
16880
+ const attachments = attachmentsFromContent(content);
16881
+ const activityAt = importMessageActivityAt(line, message);
16882
+ if (body === void 0 && attachments.length === 0) {
16883
+ return [];
16884
+ }
16885
+ return [
16886
+ {
16887
+ itemKey: `claude:${index}`,
16888
+ role,
16889
+ body: body ?? attachmentOnlyBody(role, attachments.length),
16890
+ attachments,
16891
+ activityAt
16892
+ }
16893
+ ];
16894
+ });
16895
+ }
16896
+ function importMessageActivityAt(line, payload) {
16897
+ return [
16898
+ stringValue(line.timestamp),
16899
+ stringValue(payload?.timestamp),
16900
+ stringValue(line.created_at),
16901
+ stringValue(payload?.created_at),
16902
+ stringValue(line.createdAt),
16903
+ stringValue(payload?.createdAt),
16904
+ stringValue(line.updated_at),
16905
+ stringValue(payload?.updated_at)
16906
+ ].find(
16907
+ (candidate) => candidate !== void 0 && !Number.isNaN(Date.parse(candidate))
16908
+ );
16909
+ }
16910
+ function importRole(value) {
16911
+ switch (value) {
16912
+ case "assistant":
16913
+ return "assistant";
16914
+ case "system":
16915
+ return "system";
16916
+ case "tool":
16917
+ case "tool_result":
16918
+ case "function_call_output":
16919
+ return "tool";
16920
+ default:
16921
+ return "user";
16922
+ }
16923
+ }
16924
+ function importMessageBody(textParts, _role) {
16925
+ const text2 = textParts.map((part) => part.trim()).filter((part) => !isLowSignalTranscriptMarker(part)).filter((part) => part !== "").join("\n\n").trim();
16926
+ if (text2 === "") {
16927
+ return void 0;
16928
+ }
16929
+ return text2;
16930
+ }
16931
+ function isLowSignalTranscriptMarker(value) {
16932
+ const collapsed = value.replace(/\s+/gu, " ").trim();
16933
+ return collapsed.startsWith("<turn_aborted>") || collapsed.startsWith("<subagent_notification>") || collapsed.startsWith("<environment_context>") || collapsed.startsWith("# AGENTS.md instructions") || collapsed.startsWith("# Context from my IDE setup") || collapsed.startsWith("# Context from my editor");
16934
+ }
16935
+ function attachmentOnlyBody(role, attachmentCount) {
16936
+ return `${importRoleLabel(role)} sent ${attachmentCount} attachment${attachmentCount === 1 ? "" : "s"}`;
16937
+ }
16938
+ function importRoleLabel(role) {
16939
+ switch (role) {
16940
+ case "assistant":
16941
+ return "Assistant";
16942
+ case "system":
16943
+ return "System";
16944
+ case "tool":
16945
+ return "Tool";
16946
+ case "user":
16947
+ return "User";
16948
+ }
16949
+ }
16950
+ function attachmentsFromContent(value) {
16951
+ return arrayValue(value)?.flatMap((entry, index) => {
16952
+ const item = objectValue(entry);
16953
+ if (item === void 0) {
16954
+ return [];
16955
+ }
16956
+ return attachmentFromObject(item, index);
16957
+ }) ?? [];
16958
+ }
16959
+ function attachmentFromObject(item, index) {
16960
+ const localPath = stringValue(item.path) ?? stringValue(item.file_path) ?? stringValue(item.filePath);
16961
+ const fileName = stringValue(item.name) ?? stringValue(item.file_name) ?? stringValue(item.filename);
16962
+ const contentType = stringValue(item.mime_type) ?? stringValue(item.media_type) ?? stringValue(item.content_type);
16963
+ if (localPath !== void 0) {
16964
+ return [
16965
+ {
16966
+ kind: "path",
16967
+ path: localPath,
16968
+ fileName,
16969
+ contentType
16970
+ }
16971
+ ];
16972
+ }
16973
+ const source = objectValue(item.source);
16974
+ const dataBase64 = stringValue(item.data) ?? stringValue(source?.data) ?? dataUrlBase64(item);
16975
+ const mediaType = stringValue(source?.media_type) ?? stringValue(item.media_type) ?? stringValue(item.mime_type) ?? stringValue(item.content_type);
16976
+ if (dataBase64 !== void 0 && mediaType !== void 0) {
16977
+ return [
16978
+ {
16979
+ kind: "bytes",
16980
+ dataBase64,
16981
+ fileName: fileName ?? `conversation-attachment-${index + 1}${extensionForContentType(mediaType)}`,
16982
+ contentType: mediaType
16983
+ }
16984
+ ];
16985
+ }
16986
+ return [];
16987
+ }
16988
+ function dataUrlBase64(item) {
16989
+ const imageUrl = stringValue(item.image_url) ?? stringValue(item.url);
16990
+ if (imageUrl === void 0) {
16991
+ return void 0;
16992
+ }
16993
+ const match = imageUrl.match(/^data:[^;,]+;base64,(.+)$/u);
16994
+ return match?.[1];
16995
+ }
16996
+ function extensionForContentType(contentType) {
16997
+ switch (contentType) {
16998
+ case "image/jpeg":
16999
+ return ".jpg";
17000
+ case "image/png":
17001
+ return ".png";
17002
+ case "image/gif":
17003
+ return ".gif";
17004
+ case "image/webp":
17005
+ return ".webp";
17006
+ default:
17007
+ return "";
17008
+ }
17009
+ }
17010
+ function boundedTranscriptText(path2) {
17011
+ let fd;
17012
+ try {
17013
+ const stat2 = statSync(path2);
17014
+ fd = openSync3(path2, "r");
17015
+ const firstBuffer = Buffer.alloc(
17016
+ Math.min(boundedTranscriptBytes, stat2.size)
17017
+ );
17018
+ const firstBytes = readSync(fd, firstBuffer, 0, firstBuffer.length, 0);
17019
+ const firstText = firstBuffer.subarray(0, firstBytes).toString("utf8");
17020
+ if (stat2.size <= boundedTranscriptBytes) {
17021
+ return firstText.split("\n").filter((line) => line.trim() !== "");
17022
+ }
17023
+ const lastBuffer = Buffer.alloc(boundedTranscriptBytes);
17024
+ const lastOffset = Math.max(0, stat2.size - boundedTranscriptBytes);
17025
+ const lastBytes = readSync(
17026
+ fd,
17027
+ lastBuffer,
17028
+ 0,
17029
+ lastBuffer.length,
17030
+ lastOffset
17031
+ );
17032
+ const lastText = lastBuffer.subarray(0, lastBytes).toString("utf8");
17033
+ return `${firstText}
17034
+ ${lastText}`.split("\n").filter((line) => line.trim() !== "");
17035
+ } catch (_error) {
17036
+ return [];
17037
+ } finally {
17038
+ if (fd !== void 0) {
17039
+ closeSync2(fd);
17040
+ }
17041
+ }
17042
+ }
17043
+ function firstCodexUserMessage(lines) {
17044
+ return lines.filter((line) => line.type === "response_item").map((line) => objectValue(line.payload)).filter((payload) => payload !== void 0).filter((payload) => payload.role === "user").flatMap((payload) => textFromContent(payload.content)).filter((text2) => !isInternalOnboardingDiscoveryPrompt(text2)).map(cleanConversationTitle).find((title) => title !== void 0);
17045
+ }
17046
+ function hasInternalCodexDiscoveryPrompt(lines) {
17047
+ return lines.filter((line) => line.type === "response_item").map((line) => objectValue(line.payload)).filter((payload) => payload !== void 0).filter((payload) => payload.role === "user").flatMap((payload) => textFromContent(payload.content)).some(isInternalOnboardingDiscoveryPrompt);
17048
+ }
17049
+ function firstClaudeUserMessage(lines) {
17050
+ return lines.filter((line) => line.type === "user").map((line) => objectValue(line.message)).flatMap((message) => textFromContent(message?.content)).map(cleanConversationTitle).find((title) => title !== void 0);
17051
+ }
17052
+ function textFromContent(value) {
17053
+ const direct = stringValue(value);
17054
+ if (direct !== void 0) {
17055
+ return [direct];
17056
+ }
17057
+ return arrayValue(value)?.flatMap((entry) => {
17058
+ const item = objectValue(entry);
17059
+ const itemText = stringValue(item?.text);
17060
+ return itemText === void 0 ? [] : [itemText];
17061
+ }) ?? [];
17062
+ }
17063
+ function cleanConversationTitle(value) {
17064
+ if (value === void 0) {
17065
+ return void 0;
17066
+ }
17067
+ const withoutKandanHeader = value.replace(
17068
+ /^Kandan message seq=\d+ from [^\n]+:\n\n/u,
17069
+ ""
17070
+ );
17071
+ const withoutUploadBoilerplate = withoutKandanHeader.replace(
17072
+ /\n\nWhen you create files that should be attached back to Linzumi,[\s\S]*$/u,
17073
+ ""
17074
+ );
17075
+ const structuredTaskTitle = structuredPromptTaskTitle(
17076
+ withoutUploadBoilerplate
17077
+ );
17078
+ const collapsed = withoutUploadBoilerplate.replace(/\s+/gu, " ").trim();
17079
+ if (collapsed === "") {
17080
+ return void 0;
17081
+ }
17082
+ if (setupPreambleTitle(collapsed)) {
17083
+ return structuredTaskTitle;
17084
+ }
17085
+ return truncateConversationTitle(collapsed);
17086
+ }
17087
+ function isInternalOnboardingDiscoveryPrompt(value) {
17088
+ const collapsed = value.replace(/\s+/gu, " ").trim();
17089
+ return collapsed.startsWith(
17090
+ "You are Linzumi onboarding discovery running on the user computer."
17091
+ ) || collapsed.startsWith(
17092
+ "You are Linzumi onboarding conversation discovery running on the user computer."
17093
+ );
17094
+ }
17095
+ function setupPreambleTitle(value) {
17096
+ return value.startsWith("<environment_context>") || value.startsWith("# Context from my IDE setup") || value.startsWith("# Context from my editor") || value.startsWith("# AGENTS.md instructions") || value.includes("<INSTRUCTIONS>") || value.startsWith(
17097
+ "You are Linzumi onboarding discovery running on the user computer."
17098
+ ) || value.startsWith(
17099
+ "You are Linzumi onboarding conversation discovery running on the user computer."
17100
+ ) || value.startsWith("We need answer user?");
17101
+ }
17102
+ function structuredPromptTaskTitle(value) {
17103
+ const match = value.match(/(?:^|\n)Task:\s*\n+([^\n]+)/u);
17104
+ const taskLine = match?.[1]?.replace(/\s+/gu, " ").trim();
17105
+ return taskLine === void 0 || taskLine === "" ? void 0 : truncateConversationTitle(taskLine);
17106
+ }
17107
+ function truncateConversationTitle(value) {
17108
+ return value.length > titleMaxLength ? `${value.slice(0, titleMaxLength - 3)}...` : value;
17109
+ }
17110
+ function sessionIdFromFilename(path2) {
17111
+ const fileName = basename6(path2, ".jsonl");
17112
+ return fileName === "" ? void 0 : fileName;
17113
+ }
17114
+ function compactJsonObject(value) {
17115
+ return Object.fromEntries(
17116
+ Object.entries(value).filter((entry) => {
17117
+ const entryValue = entry[1];
17118
+ return entryValue !== void 0 && entryValue !== "";
17119
+ })
17120
+ );
17121
+ }
17122
+ var boundedTranscriptBytes, titleMaxLength, defaultCandidateLimit;
17123
+ var init_onboardingConversationDiscovery = __esm({
17124
+ "src/onboardingConversationDiscovery.ts"() {
17125
+ "use strict";
17126
+ init_protocol();
17127
+ init_json();
17128
+ boundedTranscriptBytes = 64 * 1024;
17129
+ titleMaxLength = 140;
17130
+ defaultCandidateLimit = 100;
17131
+ }
17132
+ });
17133
+
17134
+ // src/onboardingProjectDiscovery.ts
17135
+ import { spawnSync as spawnSync4 } from "node:child_process";
17136
+ import { homedir as homedir10 } from "node:os";
17137
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync2 } from "node:fs";
17138
+ import { basename as basename7, dirname as dirname9, join as join14 } from "node:path";
17139
+ function discoverCurrentGitProject(args) {
17140
+ const startedAtMs = args.nowMs ?? Date.now();
17141
+ const candidates = /* @__PURE__ */ new Map();
17142
+ const searchStats = [];
17143
+ const gitStartedAtMs = Date.now();
17144
+ const topLevel = gitOutput(args.cwd, ["rev-parse", "--show-toplevel"]);
17145
+ const gitDurationMs = Date.now() - gitStartedAtMs;
17146
+ if (topLevel !== void 0) {
17147
+ mergeCandidate(
17148
+ candidates,
17149
+ projectCandidateFromWorktree(topLevel, "local_current_git", topLevel)
17150
+ );
17151
+ }
17152
+ searchStats.push({
17153
+ place: args.cwd,
17154
+ source: "git",
17155
+ duration_ms: gitDurationMs,
17156
+ result_count: topLevel === void 0 ? 0 : 1
17157
+ });
17158
+ const sourceRoots = args.sourceRoots ?? defaultLocalProjectSourceRoots(args.cwd);
17159
+ let placesSearched = 1;
17160
+ for (const sourceRoot of sourceRoots) {
17161
+ const sourceRootStartedAtMs = Date.now();
17162
+ const foundInRoot = discoverGitWorktreesInSourceRoot(sourceRoot);
17163
+ for (const worktreePath of foundInRoot) {
17164
+ mergeCandidate(
17165
+ candidates,
17166
+ projectCandidateFromWorktree(
17167
+ worktreePath,
17168
+ "local_source_root",
17169
+ topLevel
17170
+ )
17171
+ );
17172
+ }
17173
+ searchStats.push({
17174
+ place: sourceRoot,
17175
+ source: "git",
17176
+ duration_ms: Date.now() - sourceRootStartedAtMs,
17177
+ result_count: foundInRoot.length
17178
+ });
17179
+ placesSearched += 1;
17180
+ }
17181
+ const discoveredCandidates = Array.from(candidates.values()).sort(
17182
+ compareProjectCandidates
17183
+ );
17184
+ const worktreeCount = discoveredCandidates.reduce((total, candidate) => {
17185
+ const count = candidate.metadata.worktree_count;
17186
+ return typeof count === "number" ? total + count : total;
17187
+ }, 0);
17188
+ if (worktreeCount > 0) {
17189
+ searchStats.push({
17190
+ place: "git worktree list --porcelain",
17191
+ source: "git",
17192
+ duration_ms: 0,
17193
+ result_count: worktreeCount
17194
+ });
17195
+ placesSearched += 1;
17196
+ }
17197
+ return {
17198
+ candidates: discoveredCandidates,
17199
+ placesSearched,
17200
+ durationMs: Math.max(0, Date.now() - startedAtMs),
17201
+ searchStats
17202
+ };
17203
+ }
17204
+ function projectCandidateFromWorktree(worktreePath, discoveryMethod, currentTopLevel) {
17205
+ const topLevel = gitOutput(worktreePath, ["rev-parse", "--show-toplevel"]);
17206
+ if (topLevel === void 0) {
17207
+ return void 0;
17208
+ }
17209
+ const worktrees = gitWorktrees(topLevel);
17210
+ const canonicalPath = worktrees[0] ?? topLevel;
17211
+ const remoteOrigin = gitOutput(topLevel, [
17212
+ "config",
17213
+ "--get",
17214
+ "remote.origin.url"
17215
+ ]);
17216
+ const branch = gitOutput(topLevel, ["branch", "--show-current"]);
17217
+ const headSha = gitOutput(topLevel, ["rev-parse", "HEAD"]);
17218
+ const lastCommitAt = gitOutput(topLevel, ["log", "-1", "--format=%cI"]);
17219
+ const readmeName = readmeProjectName(canonicalPath);
17220
+ const displayName = readmeName ?? basename7(canonicalPath);
17221
+ const importKey = remoteOrigin ?? canonicalPath;
17222
+ const currentWorktreePath = currentTopLevel === void 0 || currentTopLevel === canonicalPath || !worktrees.includes(currentTopLevel) ? void 0 : currentTopLevel;
17223
+ return {
17224
+ importKey,
17225
+ path: canonicalPath,
17226
+ displayName,
17227
+ activityAt: lastCommitAt,
17228
+ metadata: compactJsonObject2({
17229
+ branch,
17230
+ remote_origin: remoteOrigin,
17231
+ head_sha: headSha,
17232
+ discovery_method: discoveryMethod,
17233
+ canonical_path: canonicalPath,
17234
+ current_worktree_path: currentWorktreePath,
17235
+ worktree_count: worktrees.length === 0 ? void 0 : worktrees.length,
17236
+ worktree_paths: worktrees.length === 0 ? void 0 : worktrees.slice(0, worktreePathSampleLimit),
17237
+ worktree_paths_truncated: worktrees.length > worktreePathSampleLimit ? true : void 0,
17238
+ last_commit_at: lastCommitAt,
17239
+ readme_name: readmeName
17240
+ })
17241
+ };
17242
+ }
17243
+ function mergeCandidate(candidates, candidate) {
17244
+ if (candidate === void 0) {
17245
+ return;
17246
+ }
17247
+ const existing = candidates.get(candidate.importKey);
17248
+ if (existing === void 0 || shouldReplaceCandidate(existing, candidate)) {
17249
+ candidates.set(candidate.importKey, candidate);
17250
+ }
17251
+ }
17252
+ function shouldReplaceCandidate(existing, candidate) {
17253
+ const existingMethod = existing.metadata.discovery_method;
17254
+ const candidateMethod = candidate.metadata.discovery_method;
17255
+ if (existingMethod === "local_current_git") {
17256
+ return false;
17257
+ }
17258
+ if (candidateMethod === "local_current_git") {
17259
+ return true;
17260
+ }
17261
+ const existingActivity = Date.parse(existing.activityAt ?? "");
17262
+ const candidateActivity = Date.parse(candidate.activityAt ?? "");
17263
+ if (Number.isNaN(existingActivity)) {
17264
+ return !Number.isNaN(candidateActivity);
17265
+ }
17266
+ if (Number.isNaN(candidateActivity)) {
17267
+ return false;
17268
+ }
17269
+ return candidateActivity > existingActivity;
17270
+ }
17271
+ function compareProjectCandidates(left, right) {
17272
+ const leftActivity = Date.parse(left.activityAt ?? "");
17273
+ const rightActivity = Date.parse(right.activityAt ?? "");
17274
+ if (!Number.isNaN(leftActivity) && !Number.isNaN(rightActivity)) {
17275
+ return rightActivity - leftActivity;
17276
+ }
17277
+ if (!Number.isNaN(leftActivity)) {
17278
+ return -1;
17279
+ }
17280
+ if (!Number.isNaN(rightActivity)) {
17281
+ return 1;
17282
+ }
17283
+ return left.displayName.localeCompare(right.displayName);
17284
+ }
17285
+ function discoverGitWorktreesInSourceRoot(sourceRoot) {
17286
+ if (!isDirectory(sourceRoot)) {
17287
+ return [];
17288
+ }
17289
+ try {
17290
+ return readdirSync3(sourceRoot, { withFileTypes: true }).flatMap((entry) => {
17291
+ if (!entry.isDirectory()) {
17292
+ return [];
17293
+ }
17294
+ const candidatePath = join14(sourceRoot, entry.name);
17295
+ return isGitWorktreeRoot(candidatePath) ? [candidatePath] : [];
17296
+ }).sort();
17297
+ } catch (_error) {
17298
+ return [];
17299
+ }
17300
+ }
17301
+ function isGitWorktreeRoot(path2) {
17302
+ return existsSync9(join14(path2, ".git"));
17303
+ }
17304
+ function defaultLocalProjectSourceRoots(cwd) {
17305
+ return uniqueStrings2([
17306
+ ...ancestorSourceRoots(cwd),
17307
+ ...homeSourceRoots(),
17308
+ ...externalVolumeSourceRoots()
17309
+ ]).filter(isDirectory);
17310
+ }
17311
+ function ancestorSourceRoots(cwd) {
17312
+ const sourceRootNames = /* @__PURE__ */ new Set([
17313
+ "code",
17314
+ "src",
17315
+ "projects",
17316
+ "Developer",
17317
+ "workspace",
17318
+ "work"
17319
+ ]);
17320
+ const roots = [];
17321
+ let current = cwd;
17322
+ while (current !== dirname9(current)) {
17323
+ if (sourceRootNames.has(basename7(current))) {
17324
+ roots.push(current);
17325
+ }
17326
+ current = dirname9(current);
17327
+ }
17328
+ return roots;
17329
+ }
17330
+ function homeSourceRoots() {
17331
+ const home = homedir10();
17332
+ return [
17333
+ "code",
17334
+ "src",
17335
+ "projects",
17336
+ "Developer",
17337
+ "workspace",
17338
+ "work",
17339
+ "Documents",
17340
+ "Desktop"
17341
+ ].map((directoryName) => join14(home, directoryName));
17342
+ }
17343
+ function externalVolumeSourceRoots() {
17344
+ const volumesRoot = "/Volumes";
17345
+ if (!isDirectory(volumesRoot)) {
17346
+ return [];
17347
+ }
17348
+ try {
17349
+ return readdirSync3(volumesRoot, { withFileTypes: true }).flatMap(
17350
+ (entry) => {
17351
+ if (!entry.isDirectory()) {
17352
+ return [];
17353
+ }
17354
+ const volumePath = join14(volumesRoot, entry.name);
17355
+ return ["code", "src", "projects"].map(
17356
+ (directoryName) => join14(volumePath, directoryName)
17357
+ );
17358
+ }
17359
+ );
17360
+ } catch (_error) {
17361
+ return [];
17362
+ }
17363
+ }
17364
+ function isDirectory(path2) {
17365
+ try {
17366
+ return statSync2(path2).isDirectory();
17367
+ } catch (_error) {
17368
+ return false;
17369
+ }
17370
+ }
17371
+ function uniqueStrings2(values) {
17372
+ return Array.from(new Set(values));
17373
+ }
17374
+ function gitOutput(cwd, args) {
17375
+ const result = spawnSync4("git", args, {
17376
+ cwd,
17377
+ encoding: "utf8",
17378
+ stdio: ["ignore", "pipe", "ignore"]
17379
+ });
17380
+ if (result.status !== 0) {
17381
+ return void 0;
17382
+ }
17383
+ const output = result.stdout.trim();
17384
+ return output === "" ? void 0 : output;
17385
+ }
17386
+ function gitWorktrees(cwd) {
17387
+ const output = gitOutput(cwd, ["worktree", "list", "--porcelain"]);
17388
+ if (output === void 0) {
17389
+ return [];
17390
+ }
17391
+ return output.split("\n").flatMap(
17392
+ (line) => line.startsWith("worktree ") ? [line.slice("worktree ".length)] : []
17393
+ );
17394
+ }
17395
+ function readmeProjectName(projectPath) {
17396
+ const readmePath = ["README.md", "README.markdown", "readme.md"].map((fileName) => join14(projectPath, fileName)).find((path2) => existsSync9(path2));
17397
+ if (readmePath === void 0) {
17398
+ return void 0;
17399
+ }
17400
+ try {
17401
+ const heading = readFileSync11(readmePath, "utf8").split("\n").map((line) => line.trim()).find((line) => line.startsWith("# "));
17402
+ const title = heading?.replace(/^#+\s+/u, "").trim();
17403
+ return title === void 0 || title === "" ? void 0 : title;
17404
+ } catch (_error) {
17405
+ return void 0;
17406
+ }
17407
+ }
17408
+ function compactJsonObject2(value) {
17409
+ return Object.fromEntries(
17410
+ Object.entries(value).filter((entry) => {
17411
+ const entryValue = entry[1];
17412
+ return entryValue !== void 0 && entryValue !== "";
17413
+ })
17414
+ );
17415
+ }
17416
+ var worktreePathSampleLimit;
17417
+ var init_onboardingProjectDiscovery = __esm({
17418
+ "src/onboardingProjectDiscovery.ts"() {
17419
+ "use strict";
17420
+ worktreePathSampleLimit = 25;
17421
+ }
17422
+ });
17423
+
17424
+ // src/authCache.ts
17425
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
17426
+ import { homedir as homedir11 } from "node:os";
17427
+ import { dirname as dirname10, join as join15 } from "node:path";
15939
17428
  function defaultAuthFilePath() {
15940
- const base = process.env.KANDAN_HOME ?? join12(homedir9(), ".kandan");
15941
- return join12(base, "auth.json");
17429
+ return join15(homedir11(), ".linzumi", "auth.json");
15942
17430
  }
15943
17431
  function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePath()) {
15944
- if (!existsSync8(authFilePath)) {
17432
+ if (!existsSync10(authFilePath)) {
15945
17433
  return void 0;
15946
17434
  }
15947
- const authFile = parseAuthFile(readFileSync9(authFilePath, "utf8"));
17435
+ const authFile = parseAuthFile(readFileSync12(authFilePath, "utf8"));
15948
17436
  const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
15949
17437
  const entry = authFile.local_codex_runner?.[kandanBaseUrl];
15950
17438
  if (entry === void 0 || entry.access_token.trim() === "") {
@@ -15961,12 +17449,12 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
15961
17449
  };
15962
17450
  }
15963
17451
  function readPersonalAgentDelegationToken(authFilePath) {
15964
- if (!existsSync8(authFilePath)) {
17452
+ if (!existsSync10(authFilePath)) {
15965
17453
  throw new Error(
15966
17454
  `missing personal-agent delegation auth file: ${authFilePath}`
15967
17455
  );
15968
17456
  }
15969
- const authFile = parseAuthFile(readFileSync9(authFilePath, "utf8"));
17457
+ const authFile = parseAuthFile(readFileSync12(authFilePath, "utf8"));
15970
17458
  const entry = authFile.personal_agent_delegation;
15971
17459
  if (entry === void 0 || entry.access_token.trim() === "") {
15972
17460
  throw new Error(
@@ -15984,7 +17472,7 @@ function readPersonalAgentDelegationToken(authFilePath) {
15984
17472
  }
15985
17473
  function writeCachedLocalRunnerToken(args) {
15986
17474
  const authFilePath = args.authFilePath ?? defaultAuthFilePath();
15987
- const existing = existsSync8(authFilePath) ? parseAuthFile(readFileSync9(authFilePath, "utf8")) : { version: 1 };
17475
+ const existing = existsSync10(authFilePath) ? parseAuthFile(readFileSync12(authFilePath, "utf8")) : { version: 1 };
15988
17476
  const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
15989
17477
  const issuedAt = /* @__PURE__ */ new Date();
15990
17478
  const expiresAt = args.expiresInSeconds === void 0 ? void 0 : new Date(
@@ -16002,8 +17490,8 @@ function writeCachedLocalRunnerToken(args) {
16002
17490
  }
16003
17491
  }
16004
17492
  };
16005
- mkdirSync8(dirname8(authFilePath), { recursive: true });
16006
- writeFileSync6(authFilePath, `${JSON.stringify(next, null, 2)}
17493
+ mkdirSync9(dirname10(authFilePath), { recursive: true });
17494
+ writeFileSync7(authFilePath, `${JSON.stringify(next, null, 2)}
16007
17495
  `, "utf8");
16008
17496
  return {
16009
17497
  accessToken: args.accessToken,
@@ -16421,9 +17909,9 @@ var init_threadCodexWorkerIpc = __esm({
16421
17909
 
16422
17910
  // src/signupTaskSuggestions.ts
16423
17911
  import { spawn as spawn6 } from "node:child_process";
16424
- import { mkdtempSync as mkdtempSync3, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "node:fs";
17912
+ import { mkdtempSync as mkdtempSync3, readFileSync as readFileSync13, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
16425
17913
  import { tmpdir as tmpdir2 } from "node:os";
16426
- import { join as join13 } from "node:path";
17914
+ import { join as join16 } from "node:path";
16427
17915
  async function suggestSignupTasksWithCodex(args) {
16428
17916
  const attempts = 2;
16429
17917
  let previousResponse;
@@ -16445,11 +17933,11 @@ async function suggestSignupTasksWithCodex(args) {
16445
17933
  );
16446
17934
  }
16447
17935
  async function runCodexTaskSuggestion(args) {
16448
- const tempRoot = mkdtempSync3(join13(tmpdir2(), "linzumi-signup-codex-tasks-"));
16449
- const schemaPath = join13(tempRoot, "task-suggestions.schema.json");
16450
- const outputPath = join13(tempRoot, "task-suggestions.json");
17936
+ const tempRoot = mkdtempSync3(join16(tmpdir2(), "linzumi-signup-codex-tasks-"));
17937
+ const schemaPath = join16(tempRoot, "task-suggestions.schema.json");
17938
+ const outputPath = join16(tempRoot, "task-suggestions.json");
16451
17939
  const prompt = taskSuggestionPrompt(args.previousResponse);
16452
- writeFileSync7(
17940
+ writeFileSync8(
16453
17941
  schemaPath,
16454
17942
  `${JSON.stringify(taskSuggestionJsonSchema(), null, 2)}
16455
17943
  `,
@@ -16466,7 +17954,7 @@ async function runCodexTaskSuggestion(args) {
16466
17954
  prompt
16467
17955
  })
16468
17956
  );
16469
- return readFileSync10(outputPath, "utf8");
17957
+ return readFileSync13(outputPath, "utf8");
16470
17958
  } finally {
16471
17959
  rmSync3(tempRoot, { recursive: true, force: true });
16472
17960
  }
@@ -16515,6 +18003,14 @@ function taskSuggestionPrompt(previousResponse) {
16515
18003
  "Task:",
16516
18004
  "Inspect this repository read-only and suggest exactly 5 useful quick starter tasks for Codex agents.",
16517
18005
  "",
18006
+ "GitHub context:",
18007
+ "- First check whether the GitHub CLI is available and authenticated with a cheap command such as `gh auth status`.",
18008
+ "- If `gh` is missing, unauthenticated, or this repository has no GitHub remote, skip GitHub context and rely on local files.",
18009
+ "- When `gh` is authenticated, use bounded reads such as `gh pr list --limit 30 --state all` and `gh issue list --limit 30 --state all` to inspect recent open and closed pull requests and issues.",
18010
+ "- Be mindful of GitHub rate limits, CPU usage, and network usage; prefer one or two small calls and do not paginate or crawl exhaustively.",
18011
+ "- If a GitHub call returns unauthorized, forbidden, or not found for this repository, stop GitHub lookups for this repository.",
18012
+ "- Use pull request and issue titles, states, labels, and recent activity to infer what the repo is about, what the user has been working on, and likely next tasks.",
18013
+ "",
16518
18014
  "Constraints:",
16519
18015
  "- Prefer tasks that are small, concrete, and likely to produce useful first results.",
16520
18016
  "- Do not modify files.",
@@ -16565,7 +18061,9 @@ function parseTaskSuggestionResponse(response) {
16565
18061
  const titles = tasks.map(
16566
18062
  (task) => typeof task === "object" && task !== null && !Array.isArray(task) ? task.title : void 0
16567
18063
  );
16568
- if (!titles.every((title) => typeof title === "string" && title.trim() !== "")) {
18064
+ if (!titles.every(
18065
+ (title) => typeof title === "string" && title.trim() !== "" && title.trim().length <= 120
18066
+ )) {
16569
18067
  return void 0;
16570
18068
  }
16571
18069
  return titles.map((title, index) => ({
@@ -16628,8 +18126,8 @@ var init_signupTaskSuggestions = __esm({
16628
18126
 
16629
18127
  // src/remoteCodexSandboxRunner.ts
16630
18128
  import { spawn as spawn7 } from "node:child_process";
16631
- import { existsSync as existsSync9, realpathSync as realpathSync5 } from "node:fs";
16632
- import { dirname as dirname9, isAbsolute as isAbsolute3 } from "node:path";
18129
+ import { existsSync as existsSync11, realpathSync as realpathSync5 } from "node:fs";
18130
+ import { dirname as dirname11, isAbsolute as isAbsolute3 } from "node:path";
16633
18131
  function createConfiguredRemoteCodexSandboxRunner(args) {
16634
18132
  const kind = normalizedSandboxKind(args.env.LINZUMI_REMOTE_CODEX_SANDBOX);
16635
18133
  if (kind === void 0) {
@@ -16648,7 +18146,7 @@ function createConfiguredRemoteCodexSandboxRunner(args) {
16648
18146
  }
16649
18147
  break;
16650
18148
  }
16651
- if (!existsSync9(sandboxBin)) {
18149
+ if (!existsSync11(sandboxBin)) {
16652
18150
  throw new Error(`remote Codex sandbox binary not found: ${sandboxBin}`);
16653
18151
  }
16654
18152
  return createRemoteCodexSandboxRunner({
@@ -16659,7 +18157,7 @@ function createConfiguredRemoteCodexSandboxRunner(args) {
16659
18157
  function createRemoteCodexSandboxRunner(config, deps = {}) {
16660
18158
  const resolvedDeps = {
16661
18159
  platform: deps.platform ?? process.platform,
16662
- exists: deps.exists ?? existsSync9,
18160
+ exists: deps.exists ?? existsSync11,
16663
18161
  spawnProcess: deps.spawnProcess ?? spawn7
16664
18162
  };
16665
18163
  return (request) => runSandboxInvocation(
@@ -16742,10 +18240,10 @@ function bubblewrapArgs(sandboxBin, request, exists) {
16742
18240
  key,
16743
18241
  value
16744
18242
  ]);
16745
- const readOnlyBindArgs = uniqueStrings2([
18243
+ const readOnlyBindArgs = uniqueStrings3([
16746
18244
  ...linuxReadOnlyRoots,
16747
- ...isAbsolute3(request.command) ? [dirname9(request.command)] : [],
16748
- dirname9(sandboxBin)
18245
+ ...isAbsolute3(request.command) ? [dirname11(request.command)] : [],
18246
+ dirname11(sandboxBin)
16749
18247
  ]).flatMap((path2) => exists(path2) ? ["--ro-bind", path2, path2] : []);
16750
18248
  const cwdParentDirs = parentDirs(request.cwd).flatMap((path2) => [
16751
18249
  "--dir",
@@ -16776,9 +18274,9 @@ function bubblewrapArgs(sandboxBin, request, exists) {
16776
18274
  ];
16777
18275
  }
16778
18276
  function macosSeatbeltProfile(request, exists) {
16779
- const readableRoots = uniqueStrings2([
18277
+ const readableRoots = uniqueStrings3([
16780
18278
  ...macosReadOnlyRoots,
16781
- ...isAbsolute3(request.command) ? [dirname9(request.command)] : []
18279
+ ...isAbsolute3(request.command) ? [dirname11(request.command)] : []
16782
18280
  ]).filter(exists);
16783
18281
  const readableRules = readableRoots.map((path2) => `(allow file-read* (subpath ${sandboxString(path2)}))`).join("\n");
16784
18282
  const writableTempRules = macosWritableTempRoots.filter(exists).flatMap((path2) => [
@@ -16860,11 +18358,11 @@ function parentDirs(path2) {
16860
18358
  (_part, index) => `/${parents.slice(0, index + 1).join("/")}`
16861
18359
  );
16862
18360
  }
16863
- function uniqueStrings2(values) {
18361
+ function uniqueStrings3(values) {
16864
18362
  return Array.from(new Set(values));
16865
18363
  }
16866
18364
  function existingPathAliases(path2) {
16867
- return uniqueStrings2([path2, realpathSync5(path2)]);
18365
+ return uniqueStrings3([path2, realpathSync5(path2)]);
16868
18366
  }
16869
18367
  function sandboxString(value) {
16870
18368
  return JSON.stringify(value);
@@ -16900,21 +18398,29 @@ var init_remoteCodexSandboxRunner = __esm({
16900
18398
  });
16901
18399
 
16902
18400
  // src/runner.ts
16903
- import { spawn as spawn8, spawnSync as spawnSync4 } from "node:child_process";
16904
- import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
18401
+ import { spawn as spawn8, spawnSync as spawnSync5 } from "node:child_process";
18402
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
16905
18403
  import {
16906
18404
  chmodSync as chmodSync2,
16907
18405
  lstatSync,
16908
- mkdirSync as mkdirSync9,
18406
+ mkdirSync as mkdirSync10,
16909
18407
  mkdtempSync as mkdtempSync4,
16910
- readdirSync as readdirSync2,
18408
+ readdirSync as readdirSync4,
16911
18409
  realpathSync as realpathSync6,
16912
18410
  rmSync as rmSync4,
16913
- statSync
18411
+ statSync as statSync3
16914
18412
  } from "node:fs";
18413
+ import { readFile as readFile2 } from "node:fs/promises";
16915
18414
  import { createServer as createServer3 } from "node:http";
16916
18415
  import { hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
16917
- import { dirname as dirname10, isAbsolute as isAbsolute4, join as join14, resolve as resolve7 } from "node:path";
18416
+ import {
18417
+ basename as basename8,
18418
+ dirname as dirname12,
18419
+ extname as extname2,
18420
+ isAbsolute as isAbsolute4,
18421
+ join as join17,
18422
+ resolve as resolve7
18423
+ } from "node:path";
16918
18424
  async function runLocalCodexRunner(options) {
16919
18425
  const log = makeRunnerLogger(options);
16920
18426
  const cleanup = {
@@ -17174,7 +18680,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17174
18680
  });
17175
18681
  }
17176
18682
  }
17177
- const instanceId = `codex-${randomUUID3()}`;
18683
+ const instanceId = `codex-${randomUUID4()}`;
17178
18684
  const publishLocalEditorStatus = (payload) => {
17179
18685
  void kandan.push(topic, "local_editor_status", payload).catch((error) => {
17180
18686
  log("kandan.local_editor_status_push_failed", {
@@ -17249,7 +18755,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17249
18755
  };
17250
18756
  const approveEditorPortForwardCandidate = (candidate) => {
17251
18757
  const request = pendingRequestFromCandidate({
17252
- requestId: `editor-port-forward-auto-${randomUUID3()}`,
18758
+ requestId: `editor-port-forward-auto-${randomUUID4()}`,
17253
18759
  sourceSeq: 0,
17254
18760
  candidate
17255
18761
  });
@@ -17477,7 +18983,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17477
18983
  return;
17478
18984
  }
17479
18985
  const request = pendingRequestFromCandidate({
17480
- requestId: `claude-port-forward-auto-${randomUUID3()}`,
18986
+ requestId: `claude-port-forward-auto-${randomUUID4()}`,
17481
18987
  sourceSeq,
17482
18988
  candidate
17483
18989
  });
@@ -17890,6 +19396,21 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17890
19396
  codexUrl: codexUrl ?? null,
17891
19397
  replacedRunners
17892
19398
  });
19399
+ const lifecycleWorkspaceSlug = runnerWorkspaceSlug(options);
19400
+ const lifecycleTelemetryAttributes = {
19401
+ ...lifecycleWorkspaceSlug === void 0 ? {} : { workspace: lifecycleWorkspaceSlug },
19402
+ launch_source: options.launchSource ?? "cli"
19403
+ };
19404
+ void postLifecycleEvent(
19405
+ { name: "commander.connected", attributes: lifecycleTelemetryAttributes },
19406
+ { apiUrl: options.kandanUrl }
19407
+ );
19408
+ cleanup.actions.push(
19409
+ () => postLifecycleEvent(
19410
+ { name: "commander.exited", attributes: lifecycleTelemetryAttributes },
19411
+ { apiUrl: options.kandanUrl }
19412
+ )
19413
+ );
17893
19414
  const channelSession = options.channelSession === void 0 ? void 0 : await attachChannelSession({
17894
19415
  kandan,
17895
19416
  codex,
@@ -18519,6 +20040,17 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
18519
20040
  void pushHeartbeat();
18520
20041
  return;
18521
20042
  }
20043
+ if (control.type === "import_onboarding_conversations") {
20044
+ void runLocalOnboardingConversationImport(options, log, control).catch(
20045
+ (error) => {
20046
+ log("onboarding.conversation_import_failed", {
20047
+ runnerId: options.runnerId,
20048
+ message: error instanceof Error ? error.message : String(error)
20049
+ });
20050
+ }
20051
+ );
20052
+ return;
20053
+ }
18522
20054
  void resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
18523
20055
  if (handled !== void 0) {
18524
20056
  return handled;
@@ -18555,9 +20087,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
18555
20087
  });
18556
20088
  });
18557
20089
  };
20090
+ finishRunnerStartup(
20091
+ options,
20092
+ log,
20093
+ pendingControls,
20094
+ controlDispatcher,
20095
+ handleControl
20096
+ );
20097
+ return { instanceId, codexUrl, close };
20098
+ }
20099
+ function finishRunnerStartup(options, log, pendingControls, controlDispatcher, handleControl) {
18558
20100
  controlDispatcher.value = handleControl;
18559
20101
  pendingControls.splice(0).forEach(handleControl);
18560
- return { instanceId, codexUrl, close };
20102
+ startOnboardingDiscoveryAgents(options, log);
18561
20103
  }
18562
20104
  function controlTargetsInstance(control, instanceId) {
18563
20105
  switch (control.type) {
@@ -19699,22 +21241,6 @@ ${args.developerPrompt}
19699
21241
  `;
19700
21242
  const linzumiContext = args.linzumiContext === void 0 ? "" : `
19701
21243
  ${formatLinzumiConversationContextForPrompt(args.linzumiContext)}
19702
- `;
19703
- const reviewGuidance = `
19704
- <linzumi_pr_review_guidance>
19705
- For frontend or other user-visible changes, ask whether the user wants
19706
- screenshot or screen-recording proof, ideally before/after when that helps
19707
- review the change. Screen recordings should be WebM or MP4. When the user asks
19708
- you to open or update a PR, attach or link that proof in the PR when feasible,
19709
- and state the exact blocker when it is not feasible.
19710
- </linzumi_pr_review_guidance>
19711
-
19712
- <linzumi_user_visible_writing_guide>
19713
- When you write user-visible UI, status, error, or PR-review copy, keep it short,
19714
- direct, and focused on the user's outcome. Avoid implementation-heavy sentences,
19715
- stacked clauses, and internal mechanics unless the user explicitly needs that
19716
- detail.
19717
- </linzumi_user_visible_writing_guide>
19718
21244
  `;
19719
21245
  return `<context>
19720
21246
  You are a Linzumi ${agentLabel} session launched by the Linzumi Commander.
@@ -19733,6 +21259,15 @@ Linzumi.
19733
21259
  Linzumi ${agentLabel} session: You, the inner ${agentLabel} process that performs the actual
19734
21260
  work in the approved project folder.
19735
21261
  Approved project folder: ${args.cwd}
21262
+ Coding-job metadata: User-visible goal, plan steps, step statuses, completion
21263
+ notes, primary pull request link, and worktree state shown in Linzumi's coding
21264
+ job overview.
21265
+ Plan step: A discrete chunk of work with a short title, one-line description,
21266
+ status, and completion note. It should describe an actual goal, not just a
21267
+ process verb.
21268
+ Primary pull request: The GitHub PR the job is currently driving or reviewing;
21269
+ Linzumi uses this link to hydrate PR comments, files, commits, checks, and
21270
+ deployments in the metadata overview.
19736
21271
  </term_definitions>
19737
21272
 
19738
21273
  <linzumi_mcp>
@@ -19746,11 +21281,49 @@ a concise DM to the Commander owner when the task genuinely requires it.
19746
21281
  Work only in the approved project folder unless the human explicitly asks for
19747
21282
  something else in the Linzumi thread. Start, inspect, and modify the local app
19748
21283
  from that folder. Report concise progress and exact blockers in the thread.
21284
+ Use the Linzumi MCP server named "linzumi" when you need authenticated
21285
+ workspace context or coding-job metadata tools.
21286
+ For coding-job threads, start by setting the current goal with
21287
+ linzumi_upsert_coding_job_plan, then replace the plan steps with
21288
+ linzumi_replace_coding_job_plan_steps.
21289
+ When the coding-job scope changes materially, call linzumi_rename_coding_job
21290
+ with a concise title so the Linzumi thread title and workflow-facing title
21291
+ match the work.
21292
+ As work proceeds, update each step with linzumi_update_coding_job_plan_step.
21293
+ When you open, discover, or choose the primary pull request, link it with
21294
+ linzumi_link_coding_job_pull_request.
21295
+ Before stopping, confirm the metadata overview tells the truth for a user who
21296
+ only sees that overview: completed work is completed, active work is really
21297
+ active, blocked work says why, and no stale in-progress step is left behind.
21298
+ For frontend or other user-visible changes, ask whether the user wants
21299
+ screenshot or screen-recording proof, ideally before/after when that helps
21300
+ review the change. Screen recordings should be WebM or MP4.
21301
+ When the user asks you to open or update a PR, attach or link feasible proof in
21302
+ the PR and state the exact blocker when proof is not feasible.
19749
21303
  </task_instructions>
19750
21304
 
19751
21305
  <rules>
19752
21306
  You MUST treat the Linzumi thread as the source of truth for user-facing
19753
21307
  progress.
21308
+ For coding-job threads, you MUST keep coding-job metadata current throughout
21309
+ the job.
21310
+ Plan steps MUST be fairly granular, discrete chunks of real work that a human
21311
+ can track.
21312
+ Plan step descriptions MUST stay short, usually one line and less than one full
21313
+ sentence.
21314
+ Plan steps MUST NOT be vague process-only labels such as "sync PR",
21315
+ "update PR", "verify PR", or "commit and merge" when those labels hide the
21316
+ concrete goal.
21317
+ You MUST update steps in sequence as work is completed. Do not mark a later
21318
+ step active while earlier applicable steps are still pending.
21319
+ Before you stop work, you MUST reconcile the plan so the job is not left with a
21320
+ stale active step. You may delegate metadata upkeep to a sub-agent when useful,
21321
+ but the visible job state must remain truthful.
21322
+ If the coding-job thread title is missing, undefined, or "Untitled", you MUST
21323
+ choose a concise, specific title for the job.
21324
+ User-visible UI, status, error, and PR-review copy MUST keep it short, direct,
21325
+ and focused on the user's outcome unless the user explicitly asks for
21326
+ implementation detail.
19754
21327
  You MUST keep user-visible preview servers bound to 0.0.0.0, not 127.0.0.1 or
19755
21328
  localhost, so the Linzumi secure tunnel can reach them.
19756
21329
  You MUST keep any preview or dev server as your descendant process so the
@@ -19767,16 +21340,51 @@ revert another session's work.
19767
21340
 
19768
21341
  <examples>
19769
21342
  GOOD preview command: npm run dev -- --host 0.0.0.0 --port 8787
21343
+ Why good: The preview is reachable through the Linzumi secure tunnel.
21344
+
19770
21345
  BAD preview command: npm run dev -- --host 127.0.0.1
21346
+ Why bad: Binding to localhost prevents the Linzumi secure tunnel from reaching
21347
+ the preview.
21348
+
21349
+ GOOD coding-job plan:
21350
+ - Inspect prompt sources: Locate the prompt builder, prompt guide, and tests.
21351
+ - Add metadata guidance: Patch the prompt with plan, title, and stop-state rules.
21352
+ - Verify compiled prompt: Run focused tests and inspect the generated text.
21353
+ - Push PR update: Commit owned files, push the branch, and refresh PR metadata.
21354
+ Why good: Each step is a concrete chunk of work, the list is short enough to
21355
+ scan, and the titles explain what is actually happening.
21356
+
21357
+ BAD coding-job plan:
21358
+ - Sync PR
21359
+ - Update PR
21360
+ - Verify PR
21361
+ - Commit and merge
21362
+ Why bad: These labels describe process mechanics, not the concrete work or
21363
+ product goal. A user cannot tell what the agent has learned, changed, or still
21364
+ needs to do.
21365
+
21366
+ BAD coding-job metadata state:
21367
+ - Step 1 complete, Step 2 complete, Step 3 active, Step 4 pending.
21368
+ - The agent has stopped working and sent its final response.
21369
+ Why bad: The overview still says work is active even though the agent is idle.
21370
+ Before stopping, the agent should mark Step 3 completed, blocked, or canceled
21371
+ with a truthful note.
19771
21372
  </examples>
19772
21373
  ${linzumiContext}
19773
- ${reviewGuidance}
19774
21374
  ${customPrompt}
19775
21375
  <task_reminder>
19776
21376
  You are the Commander-launched Linzumi ${agentLabel} session. Do the implementation
19777
21377
  work in the approved project folder, keep preview servers reachable through the
19778
- secure tunnel, and keep the Linzumi thread truthful.
19779
- </task_reminder>`;
21378
+ secure tunnel, and keep the Linzumi thread and coding-job metadata truthful.
21379
+ </task_reminder>
21380
+
21381
+ <output_format>
21382
+ Use normal concise Linzumi thread messages for user-facing progress and final
21383
+ answers. When the task requires files, code, commits, pull requests, previews,
21384
+ or verification output, report the concrete artifact, command, result, and any
21385
+ exact blocker. Do not emit machine-only JSON unless the user or tool explicitly
21386
+ requests it.
21387
+ </output_format>`;
19780
21388
  }
19781
21389
  function availableRunnerAgentProviders(options) {
19782
21390
  switch (options.claudeCodeRunner !== void 0 || options.claudeCodeAvailable === true) {
@@ -21573,6 +23181,869 @@ function redactedThreadRunnerCliArgs(args) {
21573
23181
  function optionalCliValue(flag, value) {
21574
23182
  return value === void 0 || value === "" ? [] : [flag, value];
21575
23183
  }
23184
+ function startOnboardingDiscoveryAgents(options, log) {
23185
+ const workspaceSlug = runnerWorkspaceSlug(options);
23186
+ if (!shouldStartOnboardingDiscoveryAgents(options, workspaceSlug)) {
23187
+ return;
23188
+ }
23189
+ const startKey = onboardingDiscoveryAgentStartKey(options, workspaceSlug);
23190
+ if (onboardingDiscoveryAgentStartKeys.has(startKey)) {
23191
+ log("onboarding.discovery_agents_already_started", {
23192
+ runnerId: options.runnerId,
23193
+ workspace: workspaceSlug
23194
+ });
23195
+ return;
23196
+ }
23197
+ onboardingDiscoveryAgentStartKeys.add(startKey);
23198
+ log("onboarding.discovery_agents_started", {
23199
+ runnerId: options.runnerId,
23200
+ workspace: workspaceSlug,
23201
+ kandanUrl: options.kandanUrl
23202
+ });
23203
+ const discoveryStartedAtMs = Date.now();
23204
+ void Promise.allSettled([
23205
+ runLocalOnboardingProjectDiscovery(options, log),
23206
+ runLocalOnboardingConversationDiscovery(options, log)
23207
+ ]).then(async (results) => {
23208
+ const initialSearchDurationMs = Date.now() - discoveryStartedAtMs;
23209
+ let successfulWriteCount = 0;
23210
+ for (const result of results) {
23211
+ if (result.status !== "fulfilled" || result.value === void 0) {
23212
+ continue;
23213
+ }
23214
+ successfulWriteCount += result.value.successfulPostStartWriteCount ?? 0;
23215
+ const initialSearchStatusWritten = await reportOnboardingDiscoveryStatus(
23216
+ options,
23217
+ log,
23218
+ {
23219
+ ...result.value,
23220
+ metadata: {
23221
+ ...result.value.metadata,
23222
+ initial_search_duration_ms: initialSearchDurationMs
23223
+ }
23224
+ }
23225
+ );
23226
+ if (initialSearchStatusWritten) {
23227
+ successfulWriteCount += 1;
23228
+ }
23229
+ }
23230
+ if (successfulWriteCount === 0) {
23231
+ onboardingDiscoveryAgentStartKeys.delete(startKey);
23232
+ log("onboarding.discovery_agents_start_key_cleared", {
23233
+ runnerId: options.runnerId,
23234
+ workspace: workspaceSlug
23235
+ });
23236
+ }
23237
+ log("onboarding.discovery_initial_search_completed", {
23238
+ runnerId: options.runnerId,
23239
+ workspace: workspaceSlug,
23240
+ duration_ms: initialSearchDurationMs,
23241
+ workers_completed: 2,
23242
+ successful_write_count: successfulWriteCount
23243
+ });
23244
+ });
23245
+ }
23246
+ function withSuccessfulWriteCount(report, successfulPostStartWriteCount) {
23247
+ return {
23248
+ ...report,
23249
+ successfulPostStartWriteCount
23250
+ };
23251
+ }
23252
+ async function addStatusWriteResult(successfulWriteCount, statusWrite) {
23253
+ const written = await statusWrite;
23254
+ return written ? successfulWriteCount + 1 : successfulWriteCount;
23255
+ }
23256
+ function successfulSettledWriteCount(results) {
23257
+ return results.filter((result) => result.status === "fulfilled").length;
23258
+ }
23259
+ async function runLocalOnboardingConversationImport(options, log, control) {
23260
+ const workspaceSlug = stringValue(control.workspace) ?? runnerWorkspaceSlug(options);
23261
+ if (workspaceSlug === void 0) {
23262
+ throw new Error("workspace_required");
23263
+ }
23264
+ const sources = onboardingConversationImportSources(control.sources);
23265
+ const importKey = onboardingConversationImportRunKey(
23266
+ options.runnerId,
23267
+ workspaceSlug,
23268
+ sources
23269
+ );
23270
+ if (onboardingConversationImportKeys.has(importKey)) {
23271
+ log("onboarding.conversation_import_skipped", {
23272
+ runnerId: options.runnerId,
23273
+ workspace: workspaceSlug,
23274
+ sources,
23275
+ reason: "already_running"
23276
+ });
23277
+ return;
23278
+ }
23279
+ onboardingConversationImportKeys.add(importKey);
23280
+ try {
23281
+ await runLocalOnboardingConversationImportOnce(
23282
+ options,
23283
+ log,
23284
+ control,
23285
+ workspaceSlug,
23286
+ sources
23287
+ );
23288
+ } finally {
23289
+ onboardingConversationImportKeys.delete(importKey);
23290
+ }
23291
+ }
23292
+ async function runLocalOnboardingConversationImportOnce(options, log, _control, workspaceSlug, sources) {
23293
+ if (sources.length === 0) {
23294
+ await reportOnboardingDiscoveryStatus(options, log, {
23295
+ phase: "conversations",
23296
+ status: "completed",
23297
+ placesSearched: 0,
23298
+ message: "No local conversation imports selected",
23299
+ currentPlace: "Codex and Claude Code stores",
23300
+ metadata: {
23301
+ worker_id: "conversations-local-import",
23302
+ worker_label: "Local conversation import",
23303
+ imported_count: 0,
23304
+ reported_count: 0
23305
+ }
23306
+ });
23307
+ return;
23308
+ }
23309
+ await reportOnboardingDiscoveryStatus(options, log, {
23310
+ phase: "conversations",
23311
+ status: "running",
23312
+ placesSearched: 0,
23313
+ message: "Importing local conversations",
23314
+ currentPlace: "Codex and Claude Code stores",
23315
+ metadata: {
23316
+ worker_id: "conversations-local-import",
23317
+ worker_label: "Local conversation import",
23318
+ imported_count: 0,
23319
+ reported_count: 0
23320
+ }
23321
+ });
23322
+ const summary = options.localConversationDiscovery?.() ?? discoverLocalConversations({
23323
+ candidateLimit: onboardingConversationImportCandidateLimit
23324
+ });
23325
+ const sourceSet = new Set(sources);
23326
+ const candidates = summary.candidates.filter((candidate) => sourceSet.has(candidate.source)).slice(0, onboardingConversationImportCandidateLimit);
23327
+ const totalCount = candidates.length;
23328
+ const client = createLinzumiMcpApiClient({
23329
+ kandanUrl: options.kandanUrl,
23330
+ accessToken: options.token,
23331
+ fetchImpl: options.fetch,
23332
+ operatingMode: "text"
23333
+ });
23334
+ let importedCount = 0;
23335
+ let failedImportCount = 0;
23336
+ let skippedImportCount = 0;
23337
+ for (const [index, candidate] of candidates.entries()) {
23338
+ try {
23339
+ const messages = readLocalConversationImportMessages(candidate);
23340
+ const response = await client.noteAgentConversation({
23341
+ workspace: workspaceSlug,
23342
+ runner_id: options.runnerId,
23343
+ source: candidate.source,
23344
+ import_key: candidate.importKey,
23345
+ import_to_office_channel: true,
23346
+ path: candidate.path,
23347
+ display_name: candidate.displayName,
23348
+ ...candidate.activityAt === void 0 ? {} : { activity_at: candidate.activityAt },
23349
+ metadata: candidate.metadata,
23350
+ title_messages: onboardingConversationTitleMessages(messages)
23351
+ });
23352
+ const officeImport = onboardingConversationOfficeImport(response);
23353
+ const threadId = stringValue(officeImport?.thread_id);
23354
+ const importStatus = stringValue(officeImport?.status);
23355
+ switch (importStatus) {
23356
+ case "failed":
23357
+ skippedImportCount += 1;
23358
+ log("onboarding.conversation_import_candidate_skipped", {
23359
+ runnerId: options.runnerId,
23360
+ workspace: workspaceSlug,
23361
+ source: candidate.source,
23362
+ import_key: candidate.importKey,
23363
+ message: stringValue(officeImport?.error) ?? "conversation_import_failed"
23364
+ });
23365
+ continue;
23366
+ default:
23367
+ break;
23368
+ }
23369
+ switch (threadId) {
23370
+ case void 0:
23371
+ break;
23372
+ default: {
23373
+ const replayedChunkCount = await replayLocalOnboardingConversation({
23374
+ client,
23375
+ options,
23376
+ workspaceSlug,
23377
+ threadId,
23378
+ candidate,
23379
+ messages
23380
+ });
23381
+ await markLocalOnboardingConversationReplayCompleted({
23382
+ client,
23383
+ workspaceSlug,
23384
+ runnerId: options.runnerId,
23385
+ candidate,
23386
+ threadId,
23387
+ officeImport,
23388
+ replayedMessageCount: messages.length,
23389
+ replayedChunkCount
23390
+ }).catch((error) => {
23391
+ log("onboarding.conversation_import_replay_completion_failed", {
23392
+ runnerId: options.runnerId,
23393
+ workspace: workspaceSlug,
23394
+ source: candidate.source,
23395
+ import_key: candidate.importKey,
23396
+ message: error instanceof Error ? error.message : String(error)
23397
+ });
23398
+ });
23399
+ break;
23400
+ }
23401
+ }
23402
+ importedCount += 1;
23403
+ } catch (error) {
23404
+ failedImportCount += 1;
23405
+ log("onboarding.conversation_import_candidate_failed", {
23406
+ runnerId: options.runnerId,
23407
+ workspace: workspaceSlug,
23408
+ source: candidate.source,
23409
+ import_key: candidate.importKey,
23410
+ message: error instanceof Error ? error.message : String(error)
23411
+ });
23412
+ }
23413
+ const processedCount = index + 1;
23414
+ if (processedCount === candidates.length || processedCount % onboardingConversationImportProgressInterval === 0) {
23415
+ await reportOnboardingDiscoveryStatus(options, log, {
23416
+ phase: "conversations",
23417
+ status: "running",
23418
+ placesSearched: summary.placesSearched,
23419
+ message: "Importing local conversations",
23420
+ currentPlace: "Codex and Claude Code stores",
23421
+ metadata: {
23422
+ worker_id: "conversations-local-import",
23423
+ worker_label: "Local conversation import",
23424
+ duration_ms: summary.durationMs,
23425
+ codex_count: sources.includes("codex") ? summary.codexCount : 0,
23426
+ claude_code_count: sources.includes("claude_code") ? summary.claudeCodeCount : 0,
23427
+ candidate_limit: onboardingConversationImportCandidateLimit,
23428
+ imported_count: importedCount,
23429
+ failed_import_count: failedImportCount,
23430
+ skipped_import_count: skippedImportCount,
23431
+ reported_count: importedCount,
23432
+ total_count: totalCount
23433
+ }
23434
+ });
23435
+ }
23436
+ }
23437
+ await reportOnboardingDiscoveryStatus(options, log, {
23438
+ phase: "conversations",
23439
+ status: "completed",
23440
+ placesSearched: summary.placesSearched,
23441
+ message: "Local conversation import finished",
23442
+ currentPlace: "Codex and Claude Code stores",
23443
+ metadata: {
23444
+ worker_id: "conversations-local-import",
23445
+ worker_label: "Local conversation import",
23446
+ duration_ms: summary.durationMs,
23447
+ codex_count: sources.includes("codex") ? summary.codexCount : 0,
23448
+ claude_code_count: sources.includes("claude_code") ? summary.claudeCodeCount : 0,
23449
+ candidate_limit: onboardingConversationImportCandidateLimit,
23450
+ imported_count: importedCount,
23451
+ failed_import_count: failedImportCount,
23452
+ skipped_import_count: skippedImportCount,
23453
+ reported_count: importedCount,
23454
+ total_count: totalCount
23455
+ }
23456
+ });
23457
+ }
23458
+ function onboardingConversationImportRunKey(runnerId, workspaceSlug, sources) {
23459
+ return `${runnerId}:${workspaceSlug}:${[...sources].sort().join(",")}`;
23460
+ }
23461
+ function onboardingConversationImportSources(value) {
23462
+ return Array.from(
23463
+ new Set(
23464
+ (arrayValue(value) ?? []).flatMap((source) => {
23465
+ switch (stringValue(source)) {
23466
+ case "codex":
23467
+ return ["codex"];
23468
+ case "claude_code":
23469
+ return ["claude_code"];
23470
+ default:
23471
+ return [];
23472
+ }
23473
+ })
23474
+ )
23475
+ ).sort();
23476
+ }
23477
+ async function replayLocalOnboardingConversation(args) {
23478
+ let replayedChunkCount = 0;
23479
+ for (const message of args.messages) {
23480
+ const chunks = onboardingConversationImportMessageChunks(message);
23481
+ for (const chunk of chunks) {
23482
+ const clientMessageId = onboardingConversationImportClientMessageId(
23483
+ args.candidate.importKey,
23484
+ chunk.itemKey
23485
+ );
23486
+ const writeMetadata = {
23487
+ import_role: message.role,
23488
+ import_source: args.candidate.source,
23489
+ ...chunk.activityAt === void 0 ? {} : { source_activity_at: chunk.activityAt }
23490
+ };
23491
+ const chunkHasAttachments = chunk.attachments.length > 0 && chunk.index === chunks.length - 1;
23492
+ if (!chunkHasAttachments) {
23493
+ await args.client.sendThreadReply({
23494
+ workspace: args.workspaceSlug,
23495
+ thread_id: args.threadId,
23496
+ body: chunk.body,
23497
+ client_message_id: clientMessageId,
23498
+ ...writeMetadata
23499
+ });
23500
+ replayedChunkCount += 1;
23501
+ continue;
23502
+ }
23503
+ const uploadResult = await uploadLocalConversationAttachments({
23504
+ client: args.client,
23505
+ options: args.options,
23506
+ workspaceSlug: args.workspaceSlug,
23507
+ candidatePath: args.candidate.path,
23508
+ body: chunk.body,
23509
+ attachments: chunk.attachments
23510
+ });
23511
+ if (uploadResult.uploadedFileIds.length > 0) {
23512
+ await args.client.attachMessageFiles({
23513
+ workspace: args.workspaceSlug,
23514
+ target: { kind: "new_message", thread_id: args.threadId },
23515
+ body: uploadResult.body,
23516
+ uploaded_file_ids: uploadResult.uploadedFileIds,
23517
+ client_message_id: clientMessageId,
23518
+ ...writeMetadata
23519
+ });
23520
+ replayedChunkCount += 1;
23521
+ continue;
23522
+ }
23523
+ await args.client.sendThreadReply({
23524
+ workspace: args.workspaceSlug,
23525
+ thread_id: args.threadId,
23526
+ body: uploadResult.body,
23527
+ client_message_id: clientMessageId,
23528
+ ...writeMetadata
23529
+ });
23530
+ replayedChunkCount += 1;
23531
+ }
23532
+ }
23533
+ return replayedChunkCount;
23534
+ }
23535
+ function onboardingConversationImportMessageChunks(message) {
23536
+ const parts = splitOnboardingConversationImportBody(message.body);
23537
+ return parts.map((body, index) => ({
23538
+ itemKey: parts.length === 1 ? message.itemKey : `${message.itemKey}:part-${index + 1}`,
23539
+ body,
23540
+ attachments: index === parts.length - 1 ? message.attachments : [],
23541
+ activityAt: message.activityAt,
23542
+ index
23543
+ }));
23544
+ }
23545
+ function splitOnboardingConversationImportBody(body) {
23546
+ const trimmed = body.trim();
23547
+ if (trimmed.length <= onboardingConversationImportMessageMaxLength) {
23548
+ return [trimmed];
23549
+ }
23550
+ const parts = [];
23551
+ let remaining = trimmed;
23552
+ while (remaining.length > onboardingConversationImportMessageMaxLength) {
23553
+ const slice = remaining.slice(
23554
+ 0,
23555
+ onboardingConversationImportMessageMaxLength
23556
+ );
23557
+ const paragraphBreak = slice.lastIndexOf("\n\n");
23558
+ const lineBreak = slice.lastIndexOf("\n");
23559
+ const splitAt = paragraphBreak > onboardingConversationImportMessageMaxLength / 2 ? paragraphBreak : lineBreak > onboardingConversationImportMessageMaxLength / 2 ? lineBreak : onboardingConversationImportMessageMaxLength;
23560
+ const part = remaining.slice(0, splitAt).trim();
23561
+ if (part !== "") {
23562
+ parts.push(part);
23563
+ }
23564
+ remaining = remaining.slice(splitAt).trim();
23565
+ }
23566
+ if (remaining !== "") {
23567
+ parts.push(remaining);
23568
+ }
23569
+ return parts;
23570
+ }
23571
+ function onboardingConversationTitleMessages(messages) {
23572
+ const indexedMessages = messages.map((message, index) => ({
23573
+ message,
23574
+ index
23575
+ }));
23576
+ const firstMessages = indexedMessages.slice(0, onboardingConversationTitleFirstMessages).map(
23577
+ ({ message, index }) => onboardingConversationTitleMessage("first", index, message)
23578
+ );
23579
+ const lastStartIndex = Math.max(
23580
+ onboardingConversationTitleFirstMessages,
23581
+ indexedMessages.length - onboardingConversationTitleLastMessages
23582
+ );
23583
+ const lastMessages = indexedMessages.slice(lastStartIndex).map(
23584
+ ({ message, index }) => onboardingConversationTitleMessage("last", index, message)
23585
+ );
23586
+ return [...firstMessages, ...lastMessages].filter(
23587
+ (message) => message !== void 0
23588
+ );
23589
+ }
23590
+ function onboardingConversationTitleMessage(position, index, message) {
23591
+ const body = message.body.trim();
23592
+ if (body === "") {
23593
+ return void 0;
23594
+ }
23595
+ return {
23596
+ position,
23597
+ index,
23598
+ role: message.role,
23599
+ body: body.length > onboardingConversationTitleBodyMaxLength ? `${body.slice(0, onboardingConversationTitleBodyMaxLength).trim()}...` : body
23600
+ };
23601
+ }
23602
+ async function markLocalOnboardingConversationReplayCompleted(args) {
23603
+ await args.client.noteAgentConversation({
23604
+ workspace: args.workspaceSlug,
23605
+ runner_id: args.runnerId,
23606
+ source: args.candidate.source,
23607
+ import_key: args.candidate.importKey,
23608
+ import_to_office_channel: true,
23609
+ path: args.candidate.path,
23610
+ display_name: args.candidate.displayName,
23611
+ ...args.candidate.activityAt === void 0 ? {} : { activity_at: args.candidate.activityAt },
23612
+ metadata: {
23613
+ ...args.candidate.metadata,
23614
+ office_channel_import: {
23615
+ ...args.officeImport ?? {},
23616
+ thread_id: args.threadId,
23617
+ replayed_message_count: args.replayedMessageCount,
23618
+ replayed_chunk_count: args.replayedChunkCount,
23619
+ replay_completed_at: (/* @__PURE__ */ new Date()).toISOString()
23620
+ }
23621
+ }
23622
+ });
23623
+ }
23624
+ function onboardingConversationOfficeImport(response) {
23625
+ const discovery = objectValue(response.discovery);
23626
+ const metadata = objectValue(discovery?.metadata);
23627
+ return objectValue(metadata?.office_channel_import);
23628
+ }
23629
+ function onboardingConversationImportClientMessageId(importKey, itemKey) {
23630
+ const importHash = createHash3("sha256").update(importKey).digest("hex");
23631
+ const itemHash = createHash3("sha256").update(itemKey).digest("hex");
23632
+ return `onboarding-import:${importHash.slice(0, 32)}:${itemHash.slice(0, 24)}`;
23633
+ }
23634
+ async function uploadLocalConversationAttachments(args) {
23635
+ const files = (await Promise.all(
23636
+ args.attachments.map(
23637
+ (attachment) => localConversationAttachmentFile(args.candidatePath, attachment)
23638
+ )
23639
+ )).filter(
23640
+ (file) => file !== void 0
23641
+ );
23642
+ if (files.length === 0) {
23643
+ return {
23644
+ body: `${args.body}
23645
+
23646
+ ${args.attachments.length} referenced attachment${args.attachments.length === 1 ? "" : "s"} were not available on disk.`,
23647
+ uploadedFileIds: []
23648
+ };
23649
+ }
23650
+ const prepare = await args.client.prepareMessageUploads({
23651
+ workspace: args.workspaceSlug,
23652
+ files: files.map((file) => ({
23653
+ file_name: file.fileName,
23654
+ content_type: file.contentType,
23655
+ size_bytes: file.bytes.byteLength
23656
+ }))
23657
+ });
23658
+ const uploads = arrayValue(prepare.uploads) ?? [];
23659
+ if (uploads.length !== files.length) {
23660
+ throw new Error("Linzumi upload prepare response count mismatch");
23661
+ }
23662
+ const preparedUploads = files.map((file, index) => {
23663
+ const upload = objectValue(uploads[index]);
23664
+ const uploadUrl = stringValue(upload?.upload_url);
23665
+ const uploadMethod = stringValue(upload?.upload_method) ?? "PUT";
23666
+ const fileId = stringValue(upload?.file_id);
23667
+ if (uploadUrl === void 0) {
23668
+ throw new Error("Linzumi upload prepare response missing upload_url");
23669
+ }
23670
+ if (fileId === void 0) {
23671
+ throw new Error("Linzumi upload prepare response missing file_id");
23672
+ }
23673
+ return {
23674
+ file,
23675
+ fileId,
23676
+ uploadMethod,
23677
+ uploadUrl
23678
+ };
23679
+ });
23680
+ const uploadResults = await Promise.allSettled(
23681
+ preparedUploads.map(async ({ file, uploadMethod, uploadUrl }) => {
23682
+ const uploadBody = file.bytes.buffer.slice(
23683
+ file.bytes.byteOffset,
23684
+ file.bytes.byteOffset + file.bytes.byteLength
23685
+ );
23686
+ const response = await (args.options.fetch ?? globalThis.fetch)(
23687
+ resolveLinzumiUploadUrl(args.options.kandanUrl, uploadUrl),
23688
+ {
23689
+ method: uploadMethod,
23690
+ headers: { "content-type": file.contentType },
23691
+ body: uploadBody
23692
+ }
23693
+ );
23694
+ if (!response.ok) {
23695
+ throw new Error(
23696
+ `Linzumi upload failed for ${file.fileName}: ${response.status} ${response.statusText}`
23697
+ );
23698
+ }
23699
+ })
23700
+ );
23701
+ const uploadedFileIds = preparedUploads.flatMap((upload, index) => {
23702
+ switch (uploadResults[index]?.status) {
23703
+ case "fulfilled":
23704
+ return [upload.fileId];
23705
+ default:
23706
+ return [];
23707
+ }
23708
+ });
23709
+ const failedUploadCount = uploadResults.filter(
23710
+ (result) => result.status === "rejected"
23711
+ ).length;
23712
+ return {
23713
+ body: failedUploadCount === 0 ? args.body : `${args.body}
23714
+
23715
+ ${failedUploadCount} referenced attachment${failedUploadCount === 1 ? "" : "s"} failed to upload.`,
23716
+ uploadedFileIds
23717
+ };
23718
+ }
23719
+ async function localConversationAttachmentFile(candidatePath, attachment) {
23720
+ switch (attachment.kind) {
23721
+ case "bytes": {
23722
+ return {
23723
+ fileName: attachment.fileName,
23724
+ contentType: attachment.contentType,
23725
+ bytes: Buffer.from(attachment.dataBase64, "base64")
23726
+ };
23727
+ }
23728
+ case "path": {
23729
+ const path2 = isAbsolute4(attachment.path) ? attachment.path : resolve7(dirname12(candidatePath), attachment.path);
23730
+ try {
23731
+ const bytes = await readFile2(path2);
23732
+ return {
23733
+ fileName: attachment.fileName ?? basename8(path2),
23734
+ contentType: attachment.contentType ?? contentTypeForPath(path2),
23735
+ bytes
23736
+ };
23737
+ } catch (_error) {
23738
+ return void 0;
23739
+ }
23740
+ }
23741
+ }
23742
+ }
23743
+ function contentTypeForPath(path2) {
23744
+ switch (extname2(path2).toLowerCase()) {
23745
+ case ".gif":
23746
+ return "image/gif";
23747
+ case ".jpeg":
23748
+ case ".jpg":
23749
+ return "image/jpeg";
23750
+ case ".png":
23751
+ return "image/png";
23752
+ case ".webp":
23753
+ return "image/webp";
23754
+ case ".json":
23755
+ case ".jsonl":
23756
+ return "application/json";
23757
+ case ".md":
23758
+ return "text/markdown";
23759
+ case ".txt":
23760
+ return "text/plain";
23761
+ default:
23762
+ return "application/octet-stream";
23763
+ }
23764
+ }
23765
+ function resolveLinzumiUploadUrl(kandanUrl, uploadUrl) {
23766
+ try {
23767
+ return new URL(uploadUrl).toString();
23768
+ } catch (_error) {
23769
+ const httpBase = kandanUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
23770
+ return new URL(uploadUrl, httpBase).toString();
23771
+ }
23772
+ }
23773
+ async function runLocalOnboardingConversationDiscovery(options, log) {
23774
+ const workspaceSlug = runnerWorkspaceSlug(options);
23775
+ if (workspaceSlug === void 0) {
23776
+ return;
23777
+ }
23778
+ await reportOnboardingDiscoveryStatus(options, log, {
23779
+ phase: "conversations",
23780
+ status: "starting",
23781
+ placesSearched: 0,
23782
+ message: "Counting local conversations",
23783
+ currentPlace: "Codex and Claude Code stores",
23784
+ metadata: {
23785
+ worker_id: "conversations-local-filesystem",
23786
+ worker_label: "Local conversation files"
23787
+ }
23788
+ });
23789
+ let successfulPostStartWriteCount = 0;
23790
+ try {
23791
+ const summary = options.localConversationDiscovery?.() ?? discoverLocalConversations();
23792
+ const reportedCandidates = summary.candidates.slice(
23793
+ 0,
23794
+ onboardingConversationDiscoveryReportLimit
23795
+ );
23796
+ const baseMetadata = {
23797
+ worker_id: "conversations-local-filesystem",
23798
+ worker_label: "Local conversation files",
23799
+ duration_ms: summary.durationMs,
23800
+ codex_count: summary.codexCount,
23801
+ claude_code_count: summary.claudeCodeCount,
23802
+ claude_code_subagent_count: summary.claudeCodeSubagentCount,
23803
+ reported_count: reportedCandidates.length,
23804
+ report_limit: onboardingConversationDiscoveryReportLimit,
23805
+ search_stats: summary.searchStats
23806
+ };
23807
+ successfulPostStartWriteCount = await addStatusWriteResult(
23808
+ successfulPostStartWriteCount,
23809
+ reportOnboardingDiscoveryStatus(options, log, {
23810
+ phase: "conversations",
23811
+ status: "running",
23812
+ placesSearched: summary.placesSearched,
23813
+ message: "Reporting local conversations",
23814
+ currentPlace: "Codex and Claude Code stores",
23815
+ metadata: baseMetadata
23816
+ })
23817
+ );
23818
+ const client = createLinzumiMcpApiClient({
23819
+ kandanUrl: options.kandanUrl,
23820
+ accessToken: options.token,
23821
+ fetchImpl: options.fetch,
23822
+ operatingMode: "text"
23823
+ });
23824
+ const reportResults = await Promise.allSettled(
23825
+ reportedCandidates.map(
23826
+ (candidate) => client.noteAgentConversation({
23827
+ workspace: workspaceSlug,
23828
+ runner_id: options.runnerId,
23829
+ source: candidate.source,
23830
+ import_key: candidate.importKey,
23831
+ path: candidate.path,
23832
+ display_name: candidate.displayName,
23833
+ ...candidate.activityAt === void 0 ? {} : { activity_at: candidate.activityAt },
23834
+ metadata: candidate.metadata
23835
+ })
23836
+ )
23837
+ );
23838
+ const failedReportCount = reportResults.filter(
23839
+ (result) => result.status === "rejected"
23840
+ ).length;
23841
+ successfulPostStartWriteCount += successfulSettledWriteCount(reportResults);
23842
+ const completedMetadata = {
23843
+ ...baseMetadata,
23844
+ failed_report_count: failedReportCount
23845
+ };
23846
+ const completedReport = {
23847
+ phase: "conversations",
23848
+ status: "completed",
23849
+ placesSearched: summary.placesSearched,
23850
+ message: "Local conversation scan finished",
23851
+ currentPlace: "Codex and Claude Code stores",
23852
+ metadata: completedMetadata
23853
+ };
23854
+ successfulPostStartWriteCount = await addStatusWriteResult(
23855
+ successfulPostStartWriteCount,
23856
+ reportOnboardingDiscoveryStatus(options, log, completedReport)
23857
+ );
23858
+ log("onboarding.local_conversation_discovery_completed", {
23859
+ runnerId: options.runnerId,
23860
+ workspace: workspaceSlug,
23861
+ duration_ms: summary.durationMs,
23862
+ codex_count: summary.codexCount,
23863
+ claude_code_count: summary.claudeCodeCount,
23864
+ claude_code_subagent_count: summary.claudeCodeSubagentCount,
23865
+ reported_count: reportedCandidates.length,
23866
+ failed_report_count: failedReportCount
23867
+ });
23868
+ return withSuccessfulWriteCount(
23869
+ completedReport,
23870
+ successfulPostStartWriteCount
23871
+ );
23872
+ } catch (error) {
23873
+ const message = error instanceof Error ? error.message : String(error);
23874
+ const failedReport = {
23875
+ phase: "conversations",
23876
+ status: "failed",
23877
+ placesSearched: 0,
23878
+ message: "Local conversation scan failed",
23879
+ currentPlace: "Codex and Claude Code stores",
23880
+ lastError: message,
23881
+ metadata: {
23882
+ worker_id: "conversations-local-filesystem",
23883
+ worker_label: "Local conversation files"
23884
+ }
23885
+ };
23886
+ successfulPostStartWriteCount = await addStatusWriteResult(
23887
+ successfulPostStartWriteCount,
23888
+ reportOnboardingDiscoveryStatus(options, log, failedReport)
23889
+ );
23890
+ log("onboarding.local_conversation_discovery_failed", {
23891
+ runnerId: options.runnerId,
23892
+ workspace: workspaceSlug,
23893
+ message
23894
+ });
23895
+ return withSuccessfulWriteCount(
23896
+ failedReport,
23897
+ successfulPostStartWriteCount
23898
+ );
23899
+ }
23900
+ }
23901
+ async function runLocalOnboardingProjectDiscovery(options, log) {
23902
+ const workspaceSlug = runnerWorkspaceSlug(options);
23903
+ if (workspaceSlug === void 0) {
23904
+ return;
23905
+ }
23906
+ await reportOnboardingDiscoveryStatus(options, log, {
23907
+ phase: "projects",
23908
+ status: "starting",
23909
+ placesSearched: 0,
23910
+ message: "Checking current Git project",
23911
+ currentPlace: options.cwd,
23912
+ currentSource: "git",
23913
+ metadata: {
23914
+ worker_id: "projects-local-current",
23915
+ worker_label: "Current Git project"
23916
+ }
23917
+ });
23918
+ let successfulPostStartWriteCount = 0;
23919
+ try {
23920
+ const summary = options.localProjectDiscovery?.() ?? discoverCurrentGitProject({ cwd: options.cwd });
23921
+ const client = createLinzumiMcpApiClient({
23922
+ kandanUrl: options.kandanUrl,
23923
+ accessToken: options.token,
23924
+ fetchImpl: options.fetch,
23925
+ operatingMode: "text"
23926
+ });
23927
+ const reportResults = await Promise.allSettled(
23928
+ summary.candidates.map(
23929
+ (candidate) => client.noteProjectDirectory({
23930
+ workspace: workspaceSlug,
23931
+ runner_id: options.runnerId,
23932
+ import_key: candidate.importKey,
23933
+ path: candidate.path,
23934
+ display_name: candidate.displayName,
23935
+ ...candidate.activityAt === void 0 ? {} : { activity_at: candidate.activityAt },
23936
+ metadata: candidate.metadata
23937
+ })
23938
+ )
23939
+ );
23940
+ const failedReportCount = reportResults.filter(
23941
+ (result) => result.status === "rejected"
23942
+ ).length;
23943
+ successfulPostStartWriteCount += successfulSettledWriteCount(reportResults);
23944
+ const completedReport = {
23945
+ phase: "projects",
23946
+ status: "completed",
23947
+ placesSearched: summary.placesSearched,
23948
+ message: summary.candidates.length === 0 ? "Current folder is not a Git project" : "Current Git project found",
23949
+ currentPlace: options.cwd,
23950
+ currentSource: "git",
23951
+ metadata: {
23952
+ worker_id: "projects-local-current",
23953
+ worker_label: "Current Git project",
23954
+ duration_ms: summary.durationMs,
23955
+ reported_count: summary.candidates.length,
23956
+ failed_report_count: failedReportCount,
23957
+ search_stats: summary.searchStats
23958
+ }
23959
+ };
23960
+ successfulPostStartWriteCount = await addStatusWriteResult(
23961
+ successfulPostStartWriteCount,
23962
+ reportOnboardingDiscoveryStatus(options, log, completedReport)
23963
+ );
23964
+ log("onboarding.local_project_discovery_completed", {
23965
+ runnerId: options.runnerId,
23966
+ workspace: workspaceSlug,
23967
+ duration_ms: summary.durationMs,
23968
+ reported_count: summary.candidates.length,
23969
+ failed_report_count: failedReportCount
23970
+ });
23971
+ return withSuccessfulWriteCount(
23972
+ completedReport,
23973
+ successfulPostStartWriteCount
23974
+ );
23975
+ } catch (error) {
23976
+ const message = error instanceof Error ? error.message : String(error);
23977
+ const failedReport = {
23978
+ phase: "projects",
23979
+ status: "failed",
23980
+ placesSearched: 0,
23981
+ message: "Current Git project scan failed",
23982
+ currentPlace: options.cwd,
23983
+ currentSource: "git",
23984
+ lastError: message,
23985
+ metadata: {
23986
+ worker_id: "projects-local-current",
23987
+ worker_label: "Current Git project"
23988
+ }
23989
+ };
23990
+ successfulPostStartWriteCount = await addStatusWriteResult(
23991
+ successfulPostStartWriteCount,
23992
+ reportOnboardingDiscoveryStatus(options, log, failedReport)
23993
+ );
23994
+ log("onboarding.local_project_discovery_failed", {
23995
+ runnerId: options.runnerId,
23996
+ workspace: workspaceSlug,
23997
+ message
23998
+ });
23999
+ return withSuccessfulWriteCount(
24000
+ failedReport,
24001
+ successfulPostStartWriteCount
24002
+ );
24003
+ }
24004
+ }
24005
+ function shouldStartOnboardingDiscoveryAgents(options, workspaceSlug = runnerWorkspaceSlug(options)) {
24006
+ return workspaceSlug !== void 0 && options.threadProcess === void 0 && options.onboardingDiscovery === "start";
24007
+ }
24008
+ async function reportOnboardingDiscoveryStatus(options, log, args) {
24009
+ const workspaceSlug = runnerWorkspaceSlug(options);
24010
+ if (workspaceSlug === void 0) {
24011
+ return false;
24012
+ }
24013
+ const client = createLinzumiMcpApiClient({
24014
+ kandanUrl: options.kandanUrl,
24015
+ accessToken: options.token,
24016
+ fetchImpl: options.fetch,
24017
+ operatingMode: "text"
24018
+ });
24019
+ const payload = {
24020
+ workspace: workspaceSlug,
24021
+ runner_id: options.runnerId,
24022
+ phase: args.phase,
24023
+ status: args.status,
24024
+ places_searched: args.placesSearched,
24025
+ message: args.message,
24026
+ ...args.currentPlace === void 0 || args.currentPlace === "" ? {} : { current_place: args.currentPlace },
24027
+ ...args.currentSource === void 0 || args.currentSource === "" ? {} : { current_source: args.currentSource },
24028
+ ...args.lastError === void 0 || args.lastError === "" ? {} : { last_error: args.lastError },
24029
+ ...args.metadata === void 0 ? {} : { metadata: args.metadata }
24030
+ };
24031
+ log("onboarding.discovery_status", payload);
24032
+ try {
24033
+ await client.noteOnboardingDiscoveryStatus(payload);
24034
+ return true;
24035
+ } catch (error) {
24036
+ log("onboarding.discovery_status_failed", {
24037
+ phase: args.phase,
24038
+ status: args.status,
24039
+ message: error instanceof Error ? error.message : String(error)
24040
+ });
24041
+ return false;
24042
+ }
24043
+ }
24044
+ function onboardingDiscoveryAgentStartKey(options, workspaceSlug) {
24045
+ return `${options.kandanUrl}\0${workspaceSlug}\0${options.runnerId}`;
24046
+ }
21576
24047
  async function startOwnedCodexAppServer(options, args = { linzumiMcp: true }) {
21577
24048
  ensureCodexProjectTrusted(options.cwd);
21578
24049
  const defaults = runnerRuntimeDefaults(options);
@@ -21629,9 +24100,9 @@ function mcpOwnerUsername(options) {
21629
24100
  return options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
21630
24101
  }
21631
24102
  function writeEphemeralMcpAuthFile(options) {
21632
- const directory = mkdtempSync4(join14(tmpdir3(), "linzumi-mcp-auth-"));
24103
+ const directory = mkdtempSync4(join17(tmpdir3(), "linzumi-mcp-auth-"));
21633
24104
  chmodSync2(directory, 448);
21634
- const path2 = join14(directory, "auth.json");
24105
+ const path2 = join17(directory, "auth.json");
21635
24106
  writeCachedLocalRunnerToken({
21636
24107
  kandanUrl: options.kandanUrl,
21637
24108
  accessToken: options.token,
@@ -21707,7 +24178,7 @@ function configuredAllowedCwds(values, options = {}) {
21707
24178
  const absolutePath = resolve7(expandUserPath(value));
21708
24179
  try {
21709
24180
  if (options.createMissing === true) {
21710
- mkdirSync9(absolutePath, { recursive: true });
24181
+ mkdirSync10(absolutePath, { recursive: true });
21711
24182
  }
21712
24183
  const realPath = realpathSync6(absolutePath);
21713
24184
  allowedCwds.push(
@@ -21744,9 +24215,9 @@ function allowedCwdProjects(allowedCwds) {
21744
24215
  });
21745
24216
  }
21746
24217
  function isGitProjectDirectory(cwd) {
21747
- const gitPath = join14(cwd, ".git");
24218
+ const gitPath = join17(cwd, ".git");
21748
24219
  try {
21749
- const gitPathStats = statSync(gitPath);
24220
+ const gitPathStats = statSync3(gitPath);
21750
24221
  return gitPathStats.isDirectory() || gitPathStats.isFile();
21751
24222
  } catch {
21752
24223
  return false;
@@ -21757,7 +24228,7 @@ function browseRunnerDirectory(control, options) {
21757
24228
  const requestedPath = stringValue(control.path) ?? currentHomeDirectory();
21758
24229
  try {
21759
24230
  const currentPath = realpathSync6(resolve7(expandUserPath(requestedPath)));
21760
- const stats = statSync(currentPath);
24231
+ const stats = statSync3(currentPath);
21761
24232
  if (!stats.isDirectory()) {
21762
24233
  return {
21763
24234
  instanceId: options.runnerId,
@@ -21767,9 +24238,9 @@ function browseRunnerDirectory(control, options) {
21767
24238
  error: "not_directory"
21768
24239
  };
21769
24240
  }
21770
- const parent = dirname10(currentPath);
21771
- const entries = readdirSync2(currentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => visibleRunnerDirectoryEntryName(entry.name)).map((entry) => {
21772
- const path2 = join14(currentPath, entry.name);
24241
+ const parent = dirname12(currentPath);
24242
+ const entries = readdirSync4(currentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => visibleRunnerDirectoryEntryName(entry.name)).map((entry) => {
24243
+ const path2 = join17(currentPath, entry.name);
21773
24244
  return {
21774
24245
  name: entry.name,
21775
24246
  path: path2,
@@ -21810,7 +24281,7 @@ function projectDirectoryName(name) {
21810
24281
  function availableProjectDirectoryName(projectsRoot, baseName, suffix = 0) {
21811
24282
  for (let nextSuffix = suffix; ; nextSuffix += 1) {
21812
24283
  const candidate = nextSuffix === 0 ? baseName : `${baseName}-${nextSuffix}`;
21813
- if (!projectPathExists(join14(projectsRoot, candidate))) {
24284
+ if (!projectPathExists(join17(projectsRoot, candidate))) {
21814
24285
  return candidate;
21815
24286
  }
21816
24287
  }
@@ -21838,9 +24309,9 @@ function createRunnerProject(control, options, allowedCwds) {
21838
24309
  error: "invalid_project_template"
21839
24310
  };
21840
24311
  }
21841
- const projectsRoot = join14(currentHomeDirectory(), "linzumi");
24312
+ const projectsRoot = join17(currentHomeDirectory(), "linzumi");
21842
24313
  const resolvedProjectDirName = template === "hello_linzumi_demo" ? availableProjectDirectoryName(projectsRoot, projectDirName) : projectDirName;
21843
- const projectPath = join14(projectsRoot, resolvedProjectDirName);
24314
+ const projectPath = join17(projectsRoot, resolvedProjectDirName);
21844
24315
  let createdProjectPath = false;
21845
24316
  try {
21846
24317
  if (template !== "hello_linzumi_demo" && projectPathExists(projectPath)) {
@@ -21852,7 +24323,7 @@ function createRunnerProject(control, options, allowedCwds) {
21852
24323
  error: "project_directory_exists"
21853
24324
  };
21854
24325
  }
21855
- mkdirSync9(projectsRoot, { recursive: true });
24326
+ mkdirSync10(projectsRoot, { recursive: true });
21856
24327
  if (template === "hello_linzumi_demo") {
21857
24328
  createdProjectPath = true;
21858
24329
  createHelloLinzumiProject({
@@ -21860,10 +24331,10 @@ function createRunnerProject(control, options, allowedCwds) {
21860
24331
  name: resolvedProjectDirName
21861
24332
  });
21862
24333
  } else {
21863
- mkdirSync9(projectPath, { recursive: false });
24334
+ mkdirSync10(projectPath, { recursive: false });
21864
24335
  createdProjectPath = true;
21865
24336
  }
21866
- const git = spawnSync4("git", ["init"], {
24337
+ const git = spawnSync5("git", ["init"], {
21867
24338
  cwd: projectPath,
21868
24339
  encoding: "utf8",
21869
24340
  env: process.env
@@ -21971,7 +24442,7 @@ async function suggestRunnerTasks(control, options, allowedCwds) {
21971
24442
  };
21972
24443
  }
21973
24444
  }
21974
- var THREAD_RUNNER_READY_TIMEOUT_MS, claudeSessionStoreSequenceHighWater, ClaudeCodeOutputPostError;
24445
+ var THREAD_RUNNER_READY_TIMEOUT_MS, onboardingDiscoveryAgentStartKeys, onboardingConversationImportKeys, onboardingConversationDiscoveryReportLimit, claudeSessionStoreSequenceHighWater, ClaudeCodeOutputPostError, onboardingConversationTitleFirstMessages, onboardingConversationTitleLastMessages, onboardingConversationTitleBodyMaxLength, onboardingConversationImportMessageMaxLength, onboardingConversationImportCandidateLimit, onboardingConversationImportProgressInterval;
21975
24446
  var init_runner = __esm({
21976
24447
  "src/runner.ts"() {
21977
24448
  "use strict";
@@ -22002,13 +24473,20 @@ var init_runner = __esm({
22002
24473
  init_runnerConsoleReporter();
22003
24474
  init_streamDeltaQueue();
22004
24475
  init_version();
24476
+ init_telemetry();
22005
24477
  init_mcpConfig();
24478
+ init_linzumiApiClient();
24479
+ init_onboardingConversationDiscovery();
24480
+ init_onboardingProjectDiscovery();
22006
24481
  init_authCache();
22007
24482
  init_threadCodexWorkerIpc();
22008
24483
  init_signupTaskSuggestions();
22009
24484
  init_userFacingErrors();
22010
24485
  init_remoteCodexSandboxRunner();
22011
24486
  THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
24487
+ onboardingDiscoveryAgentStartKeys = /* @__PURE__ */ new Set();
24488
+ onboardingConversationImportKeys = /* @__PURE__ */ new Set();
24489
+ onboardingConversationDiscoveryReportLimit = 100;
22012
24490
  claudeSessionStoreSequenceHighWater = /* @__PURE__ */ new Map();
22013
24491
  ClaudeCodeOutputPostError = class extends Error {
22014
24492
  constructor(cause) {
@@ -22017,11 +24495,17 @@ var init_runner = __esm({
22017
24495
  this.name = "ClaudeCodeOutputPostError";
22018
24496
  }
22019
24497
  };
24498
+ onboardingConversationTitleFirstMessages = 4;
24499
+ onboardingConversationTitleLastMessages = 4;
24500
+ onboardingConversationTitleBodyMaxLength = 1200;
24501
+ onboardingConversationImportMessageMaxLength = 15500;
24502
+ onboardingConversationImportCandidateLimit = 500;
24503
+ onboardingConversationImportProgressInterval = 25;
22020
24504
  }
22021
24505
  });
22022
24506
 
22023
24507
  // src/kandanTls.ts
22024
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
24508
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "node:fs";
22025
24509
  import { Agent } from "undici";
22026
24510
  import WsWebSocket from "ws";
22027
24511
  function kandanTlsTrustFromEnv() {
@@ -22032,10 +24516,10 @@ function kandanTlsTrustFromCaFile(caFile) {
22032
24516
  return void 0;
22033
24517
  }
22034
24518
  const trimmed = caFile.trim();
22035
- if (!existsSync10(trimmed)) {
24519
+ if (!existsSync12(trimmed)) {
22036
24520
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
22037
24521
  }
22038
- const ca = readFileSync11(trimmed, "utf8");
24522
+ const ca = readFileSync14(trimmed, "utf8");
22039
24523
  return {
22040
24524
  caFile: trimmed,
22041
24525
  ca,
@@ -40818,11 +43302,11 @@ var init_RemoveFileError = __esm({
40818
43302
  });
40819
43303
 
40820
43304
  // ../../node_modules/@inquirer/external-editor/dist/esm/index.js
40821
- import { spawn as spawn10, spawnSync as spawnSync5 } from "child_process";
40822
- import { readFileSync as readFileSync14, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "fs";
43305
+ import { spawn as spawn10, spawnSync as spawnSync6 } from "child_process";
43306
+ import { readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync11 } from "fs";
40823
43307
  import path from "node:path";
40824
43308
  import os from "node:os";
40825
- import { randomUUID as randomUUID4 } from "node:crypto";
43309
+ import { randomUUID as randomUUID5 } from "node:crypto";
40826
43310
  function editAsync(text2 = "", callback, fileOptions) {
40827
43311
  const editor = new ExternalEditor(text2, fileOptions);
40828
43312
  editor.runAsync((err, result) => {
@@ -40922,7 +43406,7 @@ var init_esm5 = __esm({
40922
43406
  createTemporaryFile() {
40923
43407
  try {
40924
43408
  const baseDir = this.fileOptions.dir ?? os.tmpdir();
40925
- const id = randomUUID4();
43409
+ const id = randomUUID5();
40926
43410
  const prefix = sanitizeAffix(this.fileOptions.prefix);
40927
43411
  const postfix = sanitizeAffix(this.fileOptions.postfix);
40928
43412
  const filename = `${prefix}${id}${postfix}`;
@@ -40936,14 +43420,14 @@ var init_esm5 = __esm({
40936
43420
  if (Object.prototype.hasOwnProperty.call(this.fileOptions, "mode")) {
40937
43421
  opt.mode = this.fileOptions.mode;
40938
43422
  }
40939
- writeFileSync10(this.tempFile, this.text, opt);
43423
+ writeFileSync11(this.tempFile, this.text, opt);
40940
43424
  } catch (createFileError) {
40941
43425
  throw new CreateFileError(createFileError);
40942
43426
  }
40943
43427
  }
40944
43428
  readTemporaryFile() {
40945
43429
  try {
40946
- const tempFileBuffer = readFileSync14(this.tempFile);
43430
+ const tempFileBuffer = readFileSync17(this.tempFile);
40947
43431
  if (tempFileBuffer.length === 0) {
40948
43432
  this.text = "";
40949
43433
  } else {
@@ -40966,7 +43450,7 @@ var init_esm5 = __esm({
40966
43450
  }
40967
43451
  launchEditor() {
40968
43452
  try {
40969
- const editorProcess = spawnSync5(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: "inherit" });
43453
+ const editorProcess = spawnSync6(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: "inherit" });
40970
43454
  this.lastExitStatus = editorProcess.status ?? 0;
40971
43455
  } catch (launchError) {
40972
43456
  throw new LaunchEditorError(launchError);
@@ -42308,21 +44792,21 @@ __export(signupFlow_exports, {
42308
44792
  signupTaskSuggestionWaitMessageForTest: () => signupTaskSuggestionWaitMessageForTest,
42309
44793
  toggleProjectPickerSelectionForTest: () => toggleProjectPickerSelectionForTest
42310
44794
  });
42311
- import { spawn as spawn11, spawnSync as spawnSync6 } from "node:child_process";
44795
+ import { spawn as spawn11, spawnSync as spawnSync7 } from "node:child_process";
42312
44796
  import {
42313
- existsSync as existsSync13,
44797
+ existsSync as existsSync15,
42314
44798
  constants as fsConstants,
42315
- mkdirSync as mkdirSync12,
44799
+ mkdirSync as mkdirSync13,
42316
44800
  mkdtempSync as mkdtempSync5,
42317
- readFileSync as readFileSync15,
42318
- readdirSync as readdirSync3,
44801
+ readFileSync as readFileSync18,
44802
+ readdirSync as readdirSync5,
42319
44803
  rmSync as rmSync5,
42320
- statSync as statSync2,
42321
- writeFileSync as writeFileSync11
44804
+ statSync as statSync4,
44805
+ writeFileSync as writeFileSync12
42322
44806
  } from "node:fs";
42323
44807
  import { access } from "node:fs/promises";
42324
- import { homedir as homedir12, tmpdir as tmpdir4 } from "node:os";
42325
- import { delimiter as delimiter3, dirname as dirname13, join as join17, resolve as resolve9 } from "node:path";
44808
+ import { homedir as homedir14, tmpdir as tmpdir4 } from "node:os";
44809
+ import { delimiter as delimiter3, dirname as dirname15, join as join20, resolve as resolve9 } from "node:path";
42326
44810
  import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
42327
44811
  import { emitKeypressEvents } from "node:readline";
42328
44812
  function signupHelpText() {
@@ -42334,6 +44818,7 @@ function signupHelpText() {
42334
44818
  "",
42335
44819
  "Options:",
42336
44820
  " --api-url <url> Linzumi HTTP origin, default https://serve.linzumi.com",
44821
+ " --code-server-bin <path> Custom development code-server executable",
42337
44822
  " --force-signup Ignore existing local signup auth and create a fresh login",
42338
44823
  " --no-open Print the launch URL without opening the browser",
42339
44824
  " --debug-signup Print the collected launch payload JSON after signup",
@@ -42352,6 +44837,7 @@ function signupPromptCancelMessage(error) {
42352
44837
  function parseSignupTuiOptions(args) {
42353
44838
  let serviceUrl = defaultLinzumiHttpUrl;
42354
44839
  let forceSignup = false;
44840
+ let codeServerBin;
42355
44841
  let openBrowser2 = true;
42356
44842
  let debugLaunchPayload = false;
42357
44843
  for (let index = 0; index < args.length; index += 1) {
@@ -42377,11 +44863,26 @@ function parseSignupTuiOptions(args) {
42377
44863
  index += 1;
42378
44864
  break;
42379
44865
  }
44866
+ case "--code-server-bin": {
44867
+ const value = args[index + 1];
44868
+ if (value === void 0 || value.startsWith("--")) {
44869
+ throw new Error("missing value for --code-server-bin");
44870
+ }
44871
+ codeServerBin = value;
44872
+ index += 1;
44873
+ break;
44874
+ }
42380
44875
  default:
42381
44876
  throw new Error(`invalid signup flag: ${arg ?? ""}`);
42382
44877
  }
42383
44878
  }
42384
- return { serviceUrl, forceSignup, openBrowser: openBrowser2, debugLaunchPayload };
44879
+ return {
44880
+ serviceUrl,
44881
+ forceSignup,
44882
+ ...codeServerBin === void 0 ? {} : { codeServerBin },
44883
+ openBrowser: openBrowser2,
44884
+ debugLaunchPayload
44885
+ };
42385
44886
  }
42386
44887
  function signupAuthMatchesServiceUrl(auth, serviceUrl) {
42387
44888
  return comparableServiceUrl(auth.serviceUrl) === comparableServiceUrl(serviceUrl);
@@ -42421,12 +44922,12 @@ function defaultSignupDraftStore(serviceUrl) {
42421
44922
  const path2 = defaultSignupDraftPath(serviceUrl);
42422
44923
  return {
42423
44924
  read: () => {
42424
- if (!existsSync13(path2)) {
44925
+ if (!existsSync15(path2)) {
42425
44926
  return void 0;
42426
44927
  }
42427
44928
  let parsed;
42428
44929
  try {
42429
- parsed = JSON.parse(readFileSync15(path2, "utf8"));
44930
+ parsed = JSON.parse(readFileSync18(path2, "utf8"));
42430
44931
  } catch (_error) {
42431
44932
  return void 0;
42432
44933
  }
@@ -42436,8 +44937,8 @@ function defaultSignupDraftStore(serviceUrl) {
42436
44937
  return comparableServiceUrl(parsed.serviceUrl) === comparableServiceUrl(serviceUrl) ? parsed : void 0;
42437
44938
  },
42438
44939
  write: (draft) => {
42439
- mkdirSync12(dirname13(path2), { recursive: true });
42440
- writeFileSync11(path2, `${JSON.stringify(draft, null, 2)}
44940
+ mkdirSync13(dirname15(path2), { recursive: true });
44941
+ writeFileSync12(path2, `${JSON.stringify(draft, null, 2)}
42441
44942
  `, {
42442
44943
  encoding: "utf8",
42443
44944
  mode: 384
@@ -42449,8 +44950,8 @@ function defaultSignupDraftStore(serviceUrl) {
42449
44950
  };
42450
44951
  }
42451
44952
  function defaultSignupDraftPath(serviceUrl) {
42452
- return join17(
42453
- dirname13(localConfigPath()),
44953
+ return join20(
44954
+ dirname15(localConfigPath()),
42454
44955
  `signup-draft.${localConfigScopeFileStem(serviceUrl)}.json`
42455
44956
  );
42456
44957
  }
@@ -42484,8 +44985,8 @@ function defaultSignupTaskCacheStore(serviceUrl) {
42484
44985
  }
42485
44986
  }
42486
44987
  };
42487
- mkdirSync12(dirname13(path2), { recursive: true });
42488
- writeFileSync11(path2, `${JSON.stringify(next, null, 2)}
44988
+ mkdirSync13(dirname15(path2), { recursive: true });
44989
+ writeFileSync12(path2, `${JSON.stringify(next, null, 2)}
42489
44990
  `, {
42490
44991
  encoding: "utf8",
42491
44992
  mode: 384
@@ -42494,18 +44995,18 @@ function defaultSignupTaskCacheStore(serviceUrl) {
42494
44995
  };
42495
44996
  }
42496
44997
  function defaultSignupTaskCachePath(serviceUrl) {
42497
- return join17(
42498
- dirname13(localConfigPath()),
44998
+ return join20(
44999
+ dirname15(localConfigPath()),
42499
45000
  `signup-task-cache.${localConfigScopeFileStem(serviceUrl)}.json`
42500
45001
  );
42501
45002
  }
42502
45003
  function readSignupTaskCache(path2) {
42503
- if (!existsSync13(path2)) {
45004
+ if (!existsSync15(path2)) {
42504
45005
  return { version: 1, entries: {} };
42505
45006
  }
42506
45007
  let parsed;
42507
45008
  try {
42508
- parsed = JSON.parse(readFileSync15(path2, "utf8"));
45009
+ parsed = JSON.parse(readFileSync18(path2, "utf8"));
42509
45010
  } catch (_error) {
42510
45011
  return { version: 1, entries: {} };
42511
45012
  }
@@ -42637,6 +45138,7 @@ async function runSignupFlow(deps = {}) {
42637
45138
  const preflight = deps.preflight ?? defaultPreflightRuntime();
42638
45139
  const forceSignup = deps.forceSignup === true;
42639
45140
  const serviceUrl = deps.serviceUrl ?? defaultLinzumiHttpUrl;
45141
+ const codeServerBin = deps.codeServerBin;
42640
45142
  const openBrowser2 = deps.openBrowser !== false;
42641
45143
  const debugLaunchPayload = deps.debugLaunchPayload === true;
42642
45144
  const openUrl = deps.openUrl ?? openUrlInBrowser;
@@ -42925,7 +45427,8 @@ async function runSignupFlow(deps = {}) {
42925
45427
  workspaceSlug: missionControl.workspace.slug,
42926
45428
  projectPath: starterProjectPath,
42927
45429
  selectedProjectPaths: missionControl.config.allowedCwds,
42928
- codexBin: verifiedCodexBin
45430
+ codexBin: verifiedCodexBin,
45431
+ ...codeServerBin === void 0 ? {} : { codeServerBin }
42929
45432
  });
42930
45433
  const starterTaskLaunches = await startSignupTasksForCommander(
42931
45434
  signupServerClient,
@@ -43659,6 +46162,7 @@ async function signupCommanderRunnerOptions(args, runnerId) {
43659
46162
  const editorRuntime = await resolveEditorRuntime({
43660
46163
  kandanUrl: args.serviceUrl,
43661
46164
  token: args.localRunnerAccessToken,
46165
+ customCodeServerBin: args.codeServerBin,
43662
46166
  fetchImpl: trustedFetch(trust)
43663
46167
  });
43664
46168
  const dependencyStatus = await buildRunnerDependencyStatus({
@@ -43688,6 +46192,7 @@ async function signupCommanderRunnerOptions(args, runnerId) {
43688
46192
  editorRuntime: editorRuntime.runtime,
43689
46193
  socketFactory: trustedWebSocketFactory(trust),
43690
46194
  dependencyStatus,
46195
+ onboardingDiscovery: "start",
43691
46196
  runtimeDefaults: {
43692
46197
  model: void 0,
43693
46198
  reasoningEffort: void 0,
@@ -43713,6 +46218,7 @@ function signupConnectRestartCommand(args) {
43713
46218
  "--workspace",
43714
46219
  args.workspaceSlug,
43715
46220
  ...args.codexBin === void 0 ? [] : ["--codex-bin", args.codexBin],
46221
+ ...args.codeServerBin === void 0 ? [] : ["--code-server-bin", args.codeServerBin],
43716
46222
  "--cwd",
43717
46223
  args.projectPath,
43718
46224
  ...args.serviceUrl === defaultLinzumiHttpUrl ? [] : ["--api-url", args.serviceUrl]
@@ -44385,7 +46891,7 @@ function autocompletePathSuggestions(pathInput) {
44385
46891
  try {
44386
46892
  const showHidden = prefix.startsWith(".");
44387
46893
  const currentPath = directoryExists(normalizedInput) ? [normalizedInput.replace(/\/$/, "") || "/"] : [];
44388
- const childPaths = readdirSync3(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) => join17(directoryPath, entry.name)).map((path2) => ({
46894
+ const childPaths = readdirSync5(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) => join20(directoryPath, entry.name)).map((path2) => ({
44389
46895
  path: path2,
44390
46896
  score: fuzzyPathSegmentScore(path2.split("/").at(-1) ?? path2, prefix)
44391
46897
  })).filter((candidate) => candidate.score !== void 0).sort((left, right) => {
@@ -44631,7 +47137,7 @@ async function runSignupPreflights(runtime) {
44631
47137
  function defaultPreflightRuntime() {
44632
47138
  return {
44633
47139
  cwd: process.cwd(),
44634
- homeDir: homedir12(),
47140
+ homeDir: homedir14(),
44635
47141
  probeTool,
44636
47142
  readGitEmail,
44637
47143
  discoverCodeRoots,
@@ -44776,13 +47282,13 @@ async function suggestTasksWithCodex(projectPath, retryPolicy) {
44776
47282
  );
44777
47283
  }
44778
47284
  async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolicy) {
44779
- const tempRoot = mkdtempSync5(join17(tmpdir4(), "linzumi-signup-codex-tasks-"));
44780
- const schemaPath = join17(tempRoot, "task-suggestions.schema.json");
44781
- const outputPath = join17(tempRoot, "task-suggestions.json");
47285
+ const tempRoot = mkdtempSync5(join20(tmpdir4(), "linzumi-signup-codex-tasks-"));
47286
+ const schemaPath = join20(tempRoot, "task-suggestions.schema.json");
47287
+ const outputPath = join20(tempRoot, "task-suggestions.json");
44782
47288
  const model = process.env.LINZUMI_SIGNUP_TASK_MODEL ?? "gpt-5.4-mini";
44783
47289
  const prompt = taskSuggestionPrompt2(previousResponse);
44784
47290
  const codexCommand = await resolveSignupCodexCommand();
44785
- writeFileSync11(
47291
+ writeFileSync12(
44786
47292
  schemaPath,
44787
47293
  `${JSON.stringify(taskSuggestionJsonSchema2(), null, 2)}
44788
47294
  `,
@@ -44803,7 +47309,7 @@ async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolic
44803
47309
  ),
44804
47310
  retryPolicy
44805
47311
  );
44806
- return readFileSync15(outputPath, "utf8");
47312
+ return readFileSync18(outputPath, "utf8");
44807
47313
  } finally {
44808
47314
  rmSync5(tempRoot, { recursive: true, force: true });
44809
47315
  }
@@ -44840,7 +47346,7 @@ function codexTaskSuggestionProcess2(args) {
44840
47346
  function signupCodexTaskSuggestionProcessForTest(args) {
44841
47347
  return codexTaskSuggestionProcess2(args);
44842
47348
  }
44843
- async function resolveSignupCodexCommand(env = process.env, homeDir = homedir12(), executableExists = fileIsExecutable) {
47349
+ async function resolveSignupCodexCommand(env = process.env, homeDir = homedir14(), executableExists = fileIsExecutable) {
44844
47350
  const override = firstConfiguredValue([
44845
47351
  env.LINZUMI_SIGNUP_CODEX_BIN,
44846
47352
  env.LINZUMI_CODEX_BIN
@@ -44895,7 +47401,7 @@ function resolveHomePath(path2, homeDir) {
44895
47401
  return homeDir;
44896
47402
  }
44897
47403
  if (path2.startsWith("~/")) {
44898
- return join17(homeDir, path2.slice(2));
47404
+ return join20(homeDir, path2.slice(2));
44899
47405
  }
44900
47406
  return resolve9(path2);
44901
47407
  }
@@ -44908,9 +47414,9 @@ function commandLooksPathLike(command) {
44908
47414
  }
44909
47415
  function homeManagedCodexCandidates(homeDir) {
44910
47416
  return [
44911
- join17(homeDir, ".volta", "bin", "codex"),
44912
- join17(homeDir, ".local", "bin", "codex"),
44913
- join17(homeDir, "bin", "codex")
47417
+ join20(homeDir, ".volta", "bin", "codex"),
47418
+ join20(homeDir, ".local", "bin", "codex"),
47419
+ join20(homeDir, "bin", "codex")
44914
47420
  ];
44915
47421
  }
44916
47422
  async function firstExecutablePath(paths, executableExists) {
@@ -44925,7 +47431,7 @@ function commandPathCandidates(path2) {
44925
47431
  if (path2 === void 0 || path2.trim() === "") {
44926
47432
  return [];
44927
47433
  }
44928
- return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) => join17(entry, "codex"));
47434
+ return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) => join20(entry, "codex"));
44929
47435
  }
44930
47436
  async function fileIsExecutable(path2) {
44931
47437
  try {
@@ -44950,6 +47456,14 @@ function taskSuggestionPrompt2(previousResponse) {
44950
47456
  "Task:",
44951
47457
  "Inspect this repository read-only and suggest exactly 5 useful quick starter tasks for Codex agents.",
44952
47458
  "",
47459
+ "GitHub context:",
47460
+ "- First check whether the GitHub CLI is available and authenticated with a cheap command such as `gh auth status`.",
47461
+ "- If `gh` is missing, unauthenticated, or this repository has no GitHub remote, skip GitHub context and rely on local files.",
47462
+ "- When `gh` is authenticated, use bounded reads such as `gh pr list --limit 30 --state all` and `gh issue list --limit 30 --state all` to inspect recent open and closed pull requests and issues.",
47463
+ "- Be mindful of GitHub rate limits, CPU usage, and network usage; prefer one or two small calls and do not paginate or crawl exhaustively.",
47464
+ "- If a GitHub call returns unauthorized, forbidden, or not found for this repository, stop GitHub lookups for this repository.",
47465
+ "- Use pull request and issue titles, states, labels, and recent activity to infer what the repo is about, what the user has been working on, and likely next tasks.",
47466
+ "",
44953
47467
  "Constraints:",
44954
47468
  "- Prefer tasks that are small, concrete, and likely to produce useful first results.",
44955
47469
  "- Do not modify files.",
@@ -45150,11 +47664,11 @@ function codexPreflightLocation(command, homeDir) {
45150
47664
  switch (command) {
45151
47665
  case "codex":
45152
47666
  return "on PATH";
45153
- case join17(homeDir, ".volta", "bin", "codex"):
47667
+ case join20(homeDir, ".volta", "bin", "codex"):
45154
47668
  return "via Volta";
45155
- case join17(homeDir, ".local", "bin", "codex"):
47669
+ case join20(homeDir, ".local", "bin", "codex"):
45156
47670
  return "via ~/.local/bin";
45157
- case join17(homeDir, "bin", "codex"):
47671
+ case join20(homeDir, "bin", "codex"):
45158
47672
  return "via ~/bin";
45159
47673
  default:
45160
47674
  return "from configured path";
@@ -45259,7 +47773,7 @@ function spawnSyncGit(args, cwd) {
45259
47773
  }
45260
47774
  function spawnSyncGitOutput(args, cwd) {
45261
47775
  try {
45262
- const result = spawnSync6("git", [...args], {
47776
+ const result = spawnSync7("git", [...args], {
45263
47777
  cwd,
45264
47778
  stdio: ["ignore", "pipe", "ignore"]
45265
47779
  });
@@ -45309,13 +47823,13 @@ function probeToolWithArgs(command, args, cwd) {
45309
47823
  }
45310
47824
  async function discoverCodeRoots(homeDir) {
45311
47825
  const candidates = ["src", "code", "projects"].map(
45312
- (name) => join17(homeDir, name)
47826
+ (name) => join20(homeDir, name)
45313
47827
  );
45314
- return candidates.filter((path2) => existsSync13(path2)).flatMap((path2) => discoveredProjectNames(path2));
47828
+ return candidates.filter((path2) => existsSync15(path2)).flatMap((path2) => discoveredProjectNames(path2));
45315
47829
  }
45316
47830
  function discoveredProjectNames(root) {
45317
47831
  try {
45318
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).slice(0, 3).map((entry) => `~/${root.split("/").at(-1)}/${entry.name}`);
47832
+ return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).slice(0, 3).map((entry) => `~/${root.split("/").at(-1)}/${entry.name}`);
45319
47833
  } catch {
45320
47834
  return [];
45321
47835
  }
@@ -45363,7 +47877,7 @@ function discoverProjectsFromCurrentDirectory(cwd) {
45363
47877
  }
45364
47878
  function discoverProjectsFromGuessedRoots(homeDir) {
45365
47879
  const guessedRoots = ["src", "code", "projects"].map(
45366
- (name) => join17(homeDir, name)
47880
+ (name) => join20(homeDir, name)
45367
47881
  );
45368
47882
  const projects = guessedRoots.flatMap(
45369
47883
  (root) => discoverProjectsUnderRoot(root)
@@ -45415,25 +47929,25 @@ function looksLikeProject(path2) {
45415
47929
  "pnpm-lock.yaml",
45416
47930
  "yarn.lock",
45417
47931
  "package-lock.json"
45418
- ].some((name) => existsSync13(join17(path2, name)));
47932
+ ].some((name) => existsSync15(join20(path2, name)));
45419
47933
  }
45420
47934
  function detectProjectLanguage(path2) {
45421
- if (existsSync13(join17(path2, "pyproject.toml")) || existsSync13(join17(path2, "requirements.txt"))) {
47935
+ if (existsSync15(join20(path2, "pyproject.toml")) || existsSync15(join20(path2, "requirements.txt"))) {
45422
47936
  return "Python";
45423
47937
  }
45424
- if (existsSync13(join17(path2, "Cargo.toml"))) {
47938
+ if (existsSync15(join20(path2, "Cargo.toml"))) {
45425
47939
  return "Rust";
45426
47940
  }
45427
- if (existsSync13(join17(path2, "go.mod"))) {
47941
+ if (existsSync15(join20(path2, "go.mod"))) {
45428
47942
  return "Go";
45429
47943
  }
45430
- if (existsSync13(join17(path2, "mix.exs"))) {
47944
+ if (existsSync15(join20(path2, "mix.exs"))) {
45431
47945
  return "Elixir";
45432
47946
  }
45433
- if (existsSync13(join17(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
47947
+ if (existsSync15(join20(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
45434
47948
  return "TypeScript";
45435
47949
  }
45436
- if (existsSync13(join17(path2, "package.json"))) {
47950
+ if (existsSync15(join20(path2, "package.json"))) {
45437
47951
  return "JavaScript";
45438
47952
  }
45439
47953
  return "Project";
@@ -45441,7 +47955,7 @@ function detectProjectLanguage(path2) {
45441
47955
  function packageJsonMentionsTypeScript(path2) {
45442
47956
  try {
45443
47957
  const packageJson2 = JSON.parse(
45444
- readFileSync15(join17(path2, "package.json"), "utf8")
47958
+ readFileSync18(join20(path2, "package.json"), "utf8")
45445
47959
  );
45446
47960
  return packageJson2.dependencies?.typescript !== void 0 || packageJson2.devDependencies?.typescript !== void 0;
45447
47961
  } catch {
@@ -45449,11 +47963,11 @@ function packageJsonMentionsTypeScript(path2) {
45449
47963
  }
45450
47964
  }
45451
47965
  function hasGitMetadata(path2) {
45452
- return existsSync13(join17(path2, ".git"));
47966
+ return existsSync15(join20(path2, ".git"));
45453
47967
  }
45454
47968
  function childDirectories(root) {
45455
47969
  try {
45456
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join17(root, entry.name));
47970
+ return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join20(root, entry.name));
45457
47971
  } catch {
45458
47972
  return [];
45459
47973
  }
@@ -45472,17 +47986,17 @@ function ignoredProjectDirectory(path2) {
45472
47986
  }
45473
47987
  function directoryExists(path2) {
45474
47988
  try {
45475
- return statSync2(path2).isDirectory();
47989
+ return statSync4(path2).isDirectory();
45476
47990
  } catch {
45477
47991
  return false;
45478
47992
  }
45479
47993
  }
45480
47994
  function expandHomePath(path2) {
45481
47995
  if (path2 === "~") {
45482
- return homedir12();
47996
+ return homedir14();
45483
47997
  }
45484
47998
  if (path2.startsWith("~/")) {
45485
- return join17(homedir12(), path2.slice(2));
47999
+ return join20(homedir14(), path2.slice(2));
45486
48000
  }
45487
48001
  return resolve9(path2);
45488
48002
  }
@@ -45571,8 +48085,8 @@ secure mission control for all your agents on your computers
45571
48085
  init_runner();
45572
48086
  init_claudeCodeSession();
45573
48087
  init_authCache();
45574
- import { existsSync as existsSync14, readFileSync as readFileSync16, realpathSync as realpathSync7 } from "node:fs";
45575
- import { homedir as homedir13 } from "node:os";
48088
+ import { existsSync as existsSync16, readFileSync as readFileSync19, realpathSync as realpathSync7 } from "node:fs";
48089
+ import { homedir as homedir15 } from "node:os";
45576
48090
  import { resolve as resolve10 } from "node:path";
45577
48091
  import { fileURLToPath as fileURLToPath4 } from "node:url";
45578
48092
 
@@ -45593,7 +48107,8 @@ async function resolveLocalRunnerToken(args, deps = {
45593
48107
  kandanUrl: args.kandanUrl,
45594
48108
  accessToken: cached.accessToken,
45595
48109
  workspaceSlug: args.workspaceSlug,
45596
- channelSlug: args.channelSlug
48110
+ channelSlug: args.channelSlug,
48111
+ requiredScopes: args.requiredScopes
45597
48112
  });
45598
48113
  if (cachedTokenIsUsable) {
45599
48114
  return cached.accessToken;
@@ -45633,9 +48148,9 @@ init_kandanTls();
45633
48148
  init_protocol();
45634
48149
  init_json();
45635
48150
  init_defaultUrls();
45636
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
45637
- import { dirname as dirname11, join as join15 } from "node:path";
45638
- import { homedir as homedir10 } from "node:os";
48151
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "node:fs";
48152
+ import { dirname as dirname13, join as join18 } from "node:path";
48153
+ import { homedir as homedir12 } from "node:os";
45639
48154
  async function runAgentCliCommand(args, deps = {
45640
48155
  fetchImpl: fetch,
45641
48156
  stdout: process.stdout,
@@ -46343,7 +48858,7 @@ function agentTokenFile(flags) {
46343
48858
  return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
46344
48859
  }
46345
48860
  function defaultAgentTokenFilePath() {
46346
- return join15(homedir10(), ".linzumi", "agent-token.json");
48861
+ return join18(homedir12(), ".linzumi", "agent-token.json");
46347
48862
  }
46348
48863
  function normalizedApiUrl(apiUrl) {
46349
48864
  return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
@@ -46352,11 +48867,11 @@ function authorizationHeaders(token) {
46352
48867
  return { authorization: `Bearer ${token}` };
46353
48868
  }
46354
48869
  function readOptionalTextFile(path2) {
46355
- return existsSync11(path2) ? readFileSync12(path2, "utf8") : void 0;
48870
+ return existsSync13(path2) ? readFileSync15(path2, "utf8") : void 0;
46356
48871
  }
46357
48872
  function writeTextFile(path2, content) {
46358
- mkdirSync10(dirname11(path2), { recursive: true });
46359
- writeFileSync8(path2, content);
48873
+ mkdirSync11(dirname13(path2), { recursive: true });
48874
+ writeFileSync9(path2, content);
46360
48875
  }
46361
48876
  function readStoredAgentTokenFile(path2, readTextFile = readOptionalTextFile) {
46362
48877
  const content = readTextFile(path2);
@@ -46443,27 +48958,27 @@ init_helloLinzumiProject();
46443
48958
  // src/commanderDaemon.ts
46444
48959
  init_runnerLogger();
46445
48960
  import {
46446
- existsSync as existsSync12,
46447
- closeSync as closeSync2,
46448
- mkdirSync as mkdirSync11,
46449
- openSync as openSync3,
46450
- readFileSync as readFileSync13,
48961
+ existsSync as existsSync14,
48962
+ closeSync as closeSync3,
48963
+ mkdirSync as mkdirSync12,
48964
+ openSync as openSync4,
48965
+ readFileSync as readFileSync16,
46451
48966
  watch,
46452
- writeFileSync as writeFileSync9
48967
+ writeFileSync as writeFileSync10
46453
48968
  } from "node:fs";
46454
- import { homedir as homedir11 } from "node:os";
46455
- import { dirname as dirname12, join as join16, resolve as resolve8 } from "node:path";
48969
+ import { homedir as homedir13 } from "node:os";
48970
+ import { dirname as dirname14, join as join19, resolve as resolve8 } from "node:path";
46456
48971
  import { execFileSync, spawn as spawn9 } from "node:child_process";
46457
48972
  import { fileURLToPath as fileURLToPath3 } from "node:url";
46458
48973
  var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
46459
48974
  function commanderStatusDir() {
46460
- return join16(homedir11(), ".linzumi", "commanders");
48975
+ return join19(homedir13(), ".linzumi", "commanders");
46461
48976
  }
46462
48977
  function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
46463
- return join16(statusDir, `${safeRunnerId(runnerId)}.json`);
48978
+ return join19(statusDir, `${safeRunnerId(runnerId)}.json`);
46464
48979
  }
46465
- function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
46466
- return join16(statusDir, `${safeRunnerId(runnerId)}.log`);
48980
+ function defaultCommanderLogFile(runnerId) {
48981
+ return join19(homedir13(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
46467
48982
  }
46468
48983
  function commanderLogIsConnected(log) {
46469
48984
  return connectedMarkers.some((marker) => log.includes(marker));
@@ -46472,7 +48987,7 @@ function startCommanderDaemon(options) {
46472
48987
  const statusDir = options.statusDir ?? commanderStatusDir();
46473
48988
  const statusFile = commanderStatusFile(options.runnerId, statusDir);
46474
48989
  const logFile = resolve8(
46475
- options.logFile ?? defaultCommanderLogFile(options.runnerId, statusDir)
48990
+ options.logFile ?? defaultCommanderLogFile(options.runnerId)
46476
48991
  );
46477
48992
  const entrypoint = options.entrypoint ?? currentEntrypoint();
46478
48993
  const nodeBin = options.nodeBin ?? process.execPath;
@@ -46484,10 +48999,10 @@ function startCommanderDaemon(options) {
46484
48999
  "--log-file",
46485
49000
  logFile
46486
49001
  ];
46487
- mkdirSync11(statusDir, { recursive: true });
46488
- mkdirSync11(dirname12(logFile), { recursive: true });
46489
- const out = openSync3(logFile, "a");
46490
- const err = openSync3(logFile, "a");
49002
+ mkdirSync12(statusDir, { recursive: true });
49003
+ mkdirSync12(dirname14(logFile), { recursive: true });
49004
+ const out = openSync4(logFile, "a");
49005
+ const err = openSync4(logFile, "a");
46491
49006
  writeCliAuditEvent(
46492
49007
  "process.spawn",
46493
49008
  {
@@ -46514,8 +49029,8 @@ function startCommanderDaemon(options) {
46514
49029
  },
46515
49030
  { sessionId: options.runnerId }
46516
49031
  );
46517
- closeSync2(out);
46518
- closeSync2(err);
49032
+ closeSync3(out);
49033
+ closeSync3(err);
46519
49034
  child.unref();
46520
49035
  if (child.pid === void 0) {
46521
49036
  throw new Error("commander daemon did not report a pid");
@@ -46531,21 +49046,21 @@ function startCommanderDaemon(options) {
46531
49046
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
46532
49047
  command: [nodeBin, ...command]
46533
49048
  };
46534
- writeFileSync9(statusFile, `${JSON.stringify(record, null, 2)}
49049
+ writeFileSync10(statusFile, `${JSON.stringify(record, null, 2)}
46535
49050
  `);
46536
49051
  return record;
46537
49052
  }
46538
49053
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
46539
49054
  const statusFile = commanderStatusFile(runnerId, statusDir);
46540
- if (!existsSync12(statusFile)) {
49055
+ if (!existsSync14(statusFile)) {
46541
49056
  return { status: "missing", runnerId, statusFile };
46542
49057
  }
46543
- const record = parseRecord(readFileSync13(statusFile, "utf8"));
49058
+ const record = parseRecord(readFileSync16(statusFile, "utf8"));
46544
49059
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
46545
49060
  }
46546
49061
  async function waitForCommanderDaemon(options) {
46547
49062
  const now = options.now ?? (() => Date.now());
46548
- const readTextFile = options.readTextFile ?? ((path2) => existsSync12(path2) ? readFileSync13(path2, "utf8") : void 0);
49063
+ const readTextFile = options.readTextFile ?? ((path2) => existsSync14(path2) ? readFileSync16(path2, "utf8") : void 0);
46549
49064
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
46550
49065
  const deadline = now() + options.timeoutMs;
46551
49066
  while (now() <= deadline) {
@@ -46744,6 +49259,7 @@ async function waitForFileChangeOrTimeout(path2, deadline, now, ready2 = () => f
46744
49259
 
46745
49260
  // src/index.ts
46746
49261
  init_version();
49262
+ init_telemetry();
46747
49263
 
46748
49264
  // ../../node_modules/zod/v3/external.js
46749
49265
  var external_exports = {};
@@ -54409,7 +56925,7 @@ var StdioServerTransport = class {
54409
56925
  };
54410
56926
 
54411
56927
  // src/mcpServer.ts
54412
- import { readFile as readFile2 } from "node:fs/promises";
56928
+ import { readFile as readFile3 } from "node:fs/promises";
54413
56929
 
54414
56930
  // node_modules/zod/v3/external.js
54415
56931
  var external_exports2 = {};
@@ -58456,73 +60972,19 @@ var NEVER2 = INVALID2;
58456
60972
  init_authCache();
58457
60973
  init_commanderAttachments();
58458
60974
  init_json();
58459
-
58460
- // src/linzumiApiClient.ts
58461
- init_oauth();
58462
- init_protocol();
58463
- function createLinzumiMcpApiClient(options) {
58464
- const fetchImpl = options.fetchImpl ?? fetch;
58465
- const baseUrl = kandanHttpBaseUrl(options.kandanUrl);
58466
- const apiPrefix = options.authMode === "personal-agent-delegation" ? "/api/v2/personal-agent-mcp" : "/api/v2/local-runner-mcp";
58467
- const operatingMode = options.operatingMode ?? "text";
58468
- const request = async (method, path2, params) => {
58469
- const url = new URL(path2, baseUrl);
58470
- const paramsWithMode = {
58471
- ...params,
58472
- operating_mode: operatingMode
58473
- };
58474
- const requestInit = {
58475
- method,
58476
- headers: { authorization: `Bearer ${options.accessToken}` }
58477
- };
58478
- if (method === "GET") {
58479
- for (const [key, value] of Object.entries(paramsWithMode)) {
58480
- if (value !== void 0 && value !== null) {
58481
- url.searchParams.set(key, String(value));
58482
- }
58483
- }
58484
- } else {
58485
- requestInit.headers = {
58486
- ...requestInit.headers,
58487
- "content-type": "application/json"
58488
- };
58489
- requestInit.body = JSON.stringify(paramsWithMode);
58490
- }
58491
- const response = await fetchImpl(url, requestInit);
58492
- const parsed = await response.json();
58493
- const body = isJsonObject(parsed) ? parsed : void 0;
58494
- if (body === void 0) {
58495
- throw new Error(`Linzumi MCP API returned non-object JSON from ${path2}`);
58496
- }
58497
- if (response.ok && body.ok === true) {
58498
- return body;
58499
- }
58500
- const error = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
58501
- throw new Error(`Linzumi MCP API ${path2} failed: ${error}`);
58502
- };
58503
- return {
58504
- validateAuth: () => request("GET", `${apiPrefix}/validate`, {}),
58505
- getMessage: (params) => request("GET", `${apiPrefix}/message`, params),
58506
- getThread: (params) => request("GET", `${apiPrefix}/thread`, params),
58507
- getChannel: (params) => request("GET", `${apiPrefix}/channel`, params),
58508
- listVaultSecrets: (params) => request("GET", `${apiPrefix}/vault-secrets`, params),
58509
- sendChannelMessage: (params) => request("POST", `${apiPrefix}/channel-message`, params),
58510
- prepareMessageUploads: (params) => request("POST", `${apiPrefix}/message-uploads/prepare`, params),
58511
- attachMessageFiles: (params) => request("POST", `${apiPrefix}/message-files/attach`, params),
58512
- prepareCustomEmoji: (params) => request("POST", `${apiPrefix}/custom-emoji/prepare`, params),
58513
- renameCustomEmoji: (params) => request("POST", `${apiPrefix}/custom-emoji/rename`, params),
58514
- sendThreadReply: (params) => request("POST", `${apiPrefix}/thread-reply`, params),
58515
- sendDm: (params) => request("POST", `${apiPrefix}/dm`, params),
58516
- dmOwner: (params) => request("POST", `${apiPrefix}/dm-owner`, params),
58517
- getVaultValues: (params) => request("POST", `${apiPrefix}/vault-values`, params)
58518
- };
58519
- }
58520
-
58521
- // src/mcpServer.ts
60975
+ init_linzumiApiClient();
58522
60976
  init_mcpConfig();
58523
60977
  init_oauth();
58524
60978
  init_protocol();
58525
60979
  var maxCustomEmojiNameLength = 64;
60980
+ function customEmojiMcpToolsEnabledForScope(toolScope) {
60981
+ switch (toolScope) {
60982
+ case "all":
60983
+ return true;
60984
+ case "onboarding-discovery":
60985
+ return false;
60986
+ }
60987
+ }
58526
60988
  function normalizeCustomEmojiNameForSchema(value) {
58527
60989
  return value.trim().replace(/^:+/, "").replace(/:+$/, "").toLowerCase();
58528
60990
  }
@@ -58545,6 +61007,7 @@ var mcpFlagDefinitions = /* @__PURE__ */ new Map([
58545
61007
  ["thread-id", { kind: "value" }],
58546
61008
  ["format", { kind: "value" }],
58547
61009
  ["command", { kind: "value" }],
61010
+ ["tool-scope", { kind: "value" }],
58548
61011
  ["include-token", { kind: "boolean" }],
58549
61012
  ["help", { kind: "boolean" }]
58550
61013
  ]);
@@ -58582,8 +61045,26 @@ Tools:
58582
61045
  linzumi_get_message Read one message by scoped seq or Linzumi message URL.
58583
61046
  linzumi_get_thread Read one bounded thread by ID.
58584
61047
  linzumi_get_channel Read bounded recent messages and channel metadata.
61048
+ linzumi_get_coding_job_metadata
61049
+ Read the current coding job goal, plan, and workflow metadata.
61050
+ linzumi_rename_coding_job
61051
+ Update the coding job thread and workflow-facing title.
61052
+ linzumi_upsert_coding_job_plan
61053
+ Set or update the coding job goal.
61054
+ linzumi_replace_coding_job_plan_steps
61055
+ Replace the ordered coding job plan steps.
61056
+ linzumi_update_coding_job_plan_step
61057
+ Update one coding job plan step status or note.
61058
+ linzumi_link_coding_job_pull_request
61059
+ Link the coding job to its primary GitHub pull request.
58585
61060
  linzumi_list_vault_secrets
58586
61061
  List vault secret names, descriptions, and owner scope.
61062
+ linzumi_note_project_directory
61063
+ Record one discovered local Git project directory.
61064
+ linzumi_note_agent_conversation
61065
+ Record one discovered Codex or Claude Code conversation.
61066
+ linzumi_note_onboarding_discovery_status
61067
+ Record onboarding discovery progress or failure.
58587
61068
  linzumi_send_channel_message
58588
61069
  Send a plain-text message to the scoped channel.
58589
61070
  linzumi_send_thread_reply
@@ -58619,6 +61100,7 @@ async function runMcpServer(args) {
58619
61100
  });
58620
61101
  const ownerUsername = stringValue5(values, "owner-username") ?? process.env.LINZUMI_MCP_OWNER_USERNAME;
58621
61102
  const operatingMode = mcpOperatingMode(stringValue5(values, "mode"));
61103
+ const toolScope = mcpToolScope(stringValue5(values, "tool-scope"));
58622
61104
  const cwd = stringValue5(values, "cwd") ?? process.cwd();
58623
61105
  const defaultThreadId = stringValue5(values, "thread-id") ?? process.env.LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID;
58624
61106
  const client = createLinzumiMcpApiClient({
@@ -58632,201 +61114,447 @@ async function runMcpServer(args) {
58632
61114
  version: "0.1.0"
58633
61115
  });
58634
61116
  registerEmptyMcpResources(server);
58635
- server.tool(
58636
- "linzumi_get_message",
58637
- "Read one Linzumi message by scoped message_seq/message_id or a Linzumi message URL, with optional bounded before/after context.",
58638
- {
58639
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58640
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58641
- message_id: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Message seq inside the scoped channel."),
58642
- message_seq: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Alias for message_id."),
58643
- message_url: external_exports2.string().optional().describe("Full Linzumi message URL with #message-<seq>."),
58644
- before: external_exports2.number().int().min(0).max(100).optional().describe("Number of visible messages before the target."),
58645
- after: external_exports2.number().int().min(0).max(100).optional().describe("Number of visible messages after the target.")
58646
- },
58647
- async (params) => mcpJsonResult(await client.getMessage(params))
58648
- );
58649
- server.tool(
58650
- "linzumi_get_thread",
58651
- "Read one Linzumi thread by thread_id in the scoped channel, including the parent message and bounded replies.",
58652
- {
58653
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58654
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58655
- thread_id: external_exports2.string().uuid().describe("Linzumi thread UUID."),
58656
- limit: external_exports2.number().int().min(1).max(100).optional().describe("Maximum replies to return."),
58657
- before_seq: external_exports2.number().int().positive().optional().describe("Return replies before this channel seq.")
58658
- },
58659
- async (params) => mcpJsonResult(await client.getThread(params))
58660
- );
58661
- server.tool(
58662
- "linzumi_get_channel",
58663
- "Read scoped Linzumi channel metadata and bounded recent channel messages.",
58664
- {
58665
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58666
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58667
- limit: external_exports2.number().int().min(1).max(100).optional().describe("Maximum messages to return."),
58668
- before_seq: external_exports2.number().int().positive().optional().describe("Return messages before this channel seq.")
58669
- },
58670
- async (params) => mcpJsonResult(await client.getChannel(params))
58671
- );
58672
- server.tool(
58673
- "linzumi_upload_files",
58674
- "Upload local files to Linzumi and attach them to a new, exact, or latest assistant message. Use silently; do not mention this tool in the visible reply.",
58675
- {
58676
- target: external_exports2.object({
58677
- kind: external_exports2.enum(["new_message", "message_seq", "latest_assistant_message"]).describe(
58678
- "new_message creates a message; message_seq edits an exact message; latest_assistant_message edits the latest assistant message in the scoped thread/channel."
61117
+ if (toolScope === "all") {
61118
+ server.tool(
61119
+ "linzumi_get_message",
61120
+ "Read one Linzumi message by scoped message_seq/message_id or a Linzumi message URL, with optional bounded before/after context.",
61121
+ {
61122
+ workspace: external_exports2.string().optional().describe(
61123
+ "Workspace slug. Omit to use the authenticated token scope."
58679
61124
  ),
58680
- message_seq: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Required when kind is message_seq."),
58681
- thread_id: external_exports2.string().optional().describe(
58682
- "Thread UUID. Defaults to the active Linzumi thread when the MCP server was launched with one."
61125
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61126
+ message_id: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Message seq inside the scoped channel."),
61127
+ message_seq: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Alias for message_id."),
61128
+ message_url: external_exports2.string().optional().describe("Full Linzumi message URL with #message-<seq>."),
61129
+ before: external_exports2.number().int().min(0).max(100).optional().describe("Number of visible messages before the target."),
61130
+ after: external_exports2.number().int().min(0).max(100).optional().describe("Number of visible messages after the target.")
61131
+ },
61132
+ async (params) => mcpJsonResult(await client.getMessage(params))
61133
+ );
61134
+ server.tool(
61135
+ "linzumi_get_thread",
61136
+ "Read one Linzumi thread by thread_id in the scoped channel, including the parent message and bounded replies.",
61137
+ {
61138
+ workspace: external_exports2.string().optional().describe(
61139
+ "Workspace slug. Omit to use the authenticated token scope."
61140
+ ),
61141
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61142
+ thread_id: external_exports2.string().uuid().describe("Linzumi thread UUID."),
61143
+ limit: external_exports2.number().int().min(1).max(100).optional().describe("Maximum replies to return."),
61144
+ before_seq: external_exports2.number().int().positive().optional().describe("Return replies before this channel seq.")
61145
+ },
61146
+ async (params) => mcpJsonResult(await client.getThread(params))
61147
+ );
61148
+ server.tool(
61149
+ "linzumi_get_channel",
61150
+ "Read scoped Linzumi channel metadata and bounded recent channel messages.",
61151
+ {
61152
+ workspace: external_exports2.string().optional().describe(
61153
+ "Workspace slug. Omit to use the authenticated token scope."
61154
+ ),
61155
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61156
+ limit: external_exports2.number().int().min(1).max(100).optional().describe("Maximum messages to return."),
61157
+ before_seq: external_exports2.number().int().positive().optional().describe("Return messages before this channel seq.")
61158
+ },
61159
+ async (params) => mcpJsonResult(await client.getChannel(params))
61160
+ );
61161
+ server.tool(
61162
+ "linzumi_get_coding_job_metadata",
61163
+ "Read the active coding job workflow metadata for a thread, including the agent-owned goal, ordered plan steps, step lock versions, and workflow status.",
61164
+ {
61165
+ workspace: external_exports2.string().optional().describe(
61166
+ "Workspace slug. Omit to use the authenticated token scope."
61167
+ ),
61168
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61169
+ thread_id: external_exports2.string().uuid().optional().describe(
61170
+ "Linzumi thread UUID. Defaults to the active coding job thread."
58683
61171
  )
58684
- }).describe(
58685
- "Attachment target. Do not guess; use new_message when there is no previous assistant message."
58686
- ),
58687
- files: external_exports2.array(external_exports2.string().min(1)).min(1).max(16).describe(
58688
- "Local file paths to upload. Paths must be non-hidden supported file types inside the approved workspace or Downloads."
58689
- ),
58690
- body: external_exports2.string().min(1).max(2e4).optional().describe(
58691
- "Required for new_message. Optional replacement body for message_seq/latest_assistant_message; when omitted, the existing message body is preserved."
58692
- ),
58693
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58694
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope.")
58695
- },
58696
- async (params) => {
58697
- const target = targetWithDefaultThread(
58698
- params.target,
58699
- defaultThreadId
58700
- );
58701
- const result = await uploadFilesWithClient({
58702
- client,
58703
- cwd,
58704
- kandanUrl,
58705
- target,
58706
- files: params.files,
58707
- workspace: params.workspace,
58708
- channel: params.channel,
58709
- body: params.body
58710
- });
58711
- return mcpJsonResult(result);
58712
- }
58713
- );
58714
- server.tool(
58715
- "linzumi_list_vault_secrets",
58716
- "List vault secret names, descriptions, and whether each secret is personal or workspace-owned. Does not return secret values.",
58717
- {},
58718
- async (params) => mcpJsonResult(await client.listVaultSecrets(params))
58719
- );
58720
- server.tool(
58721
- "linzumi_upload_custom_emoji",
58722
- "Upload a local image as a workspace custom emoji. For generated emoji art, create a transparent PNG with a simple centered subject, crisp edges, strong contrast, minimal tiny details, and enough outline/highlight separation to read at 16-24px on both dark and light backgrounds. Use lowercase shortcode names without colons unless preserving a user-provided :name:.",
58723
- {
58724
- name: customEmojiNameSchema.describe(
58725
- "Custom emoji shortcode name, with or without surrounding colons."
58726
- ),
58727
- file: external_exports2.string().min(1).describe(
58728
- "Local PNG/WebP/GIF/JPEG file path to upload. Prefer transparent PNG for generated emoji."
58729
- ),
58730
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope.")
58731
- },
58732
- async (params) => mcpJsonResult(
58733
- await uploadCustomEmojiWithClient({
58734
- client,
58735
- cwd,
58736
- kandanUrl,
58737
- name: params.name,
58738
- file: params.file,
58739
- workspace: params.workspace
58740
- })
58741
- )
58742
- );
58743
- server.tool(
58744
- "linzumi_rename_custom_emoji",
58745
- "Rename an existing workspace custom emoji. The new name may include surrounding colons; Linzumi stores the normalized shortcode without colons.",
58746
- {
58747
- emoji_id: external_exports2.union([external_exports2.string(), external_exports2.number().int().positive()]).describe(
58748
- "Workspace custom emoji id returned by linzumi_upload_custom_emoji or workspace emoji listings."
58749
- ),
58750
- name: customEmojiNameSchema.describe(
58751
- "New shortcode name, with or without surrounding colons."
58752
- ),
58753
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope.")
58754
- },
58755
- async (params) => mcpJsonResult(await client.renameCustomEmoji(params))
58756
- );
58757
- server.tool(
58758
- "linzumi_dm_owner",
58759
- "Send a plain-text DM to the configured Commander owner. Requires dm.write and a visible owner username.",
58760
- {
58761
- owner_username: external_exports2.string().optional().describe(
58762
- "Owner username. Defaults to LINZUMI_MCP_OWNER_USERNAME or --owner-username."
58763
- ),
58764
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58765
- channel: external_exports2.string().optional().describe("Source channel slug used to verify owner visibility."),
58766
- body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to DM.")
58767
- },
58768
- async (params) => {
58769
- const owner = params.owner_username ?? ownerUsername;
58770
- if (owner === void 0) {
58771
- throw new Error("owner_username is required for linzumi_dm_owner");
58772
- }
58773
- return mcpJsonResult(
58774
- await client.dmOwner({
58775
- ...params,
58776
- owner_username: owner
61172
+ },
61173
+ async (params) => mcpJsonResult(
61174
+ await client.getCodingJobMetadata(
61175
+ paramsWithDefaultThread(params, defaultThreadId)
61176
+ )
61177
+ )
61178
+ );
61179
+ server.tool(
61180
+ "linzumi_rename_coding_job",
61181
+ "Update the current coding job thread title and workflow-facing title. Use this when the job scope changes materially so the Linzumi thread list and metadata overview stay accurate.",
61182
+ {
61183
+ workspace: external_exports2.string().optional().describe(
61184
+ "Workspace slug. Omit to use the authenticated token scope."
61185
+ ),
61186
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61187
+ thread_id: external_exports2.string().uuid().optional().describe(
61188
+ "Linzumi thread UUID. Defaults to the active coding job thread."
61189
+ ),
61190
+ title: external_exports2.string().min(1).max(100).describe("Concise user-visible title for this coding job.")
61191
+ },
61192
+ async (params) => mcpJsonResult(
61193
+ await client.renameCodingJob(
61194
+ paramsWithDefaultThread(params, defaultThreadId)
61195
+ )
61196
+ )
61197
+ );
61198
+ server.tool(
61199
+ "linzumi_upsert_coding_job_plan",
61200
+ "Set or update the current coding job goal. Use this at job start and whenever the goal changes materially.",
61201
+ {
61202
+ workspace: external_exports2.string().optional().describe(
61203
+ "Workspace slug. Omit to use the authenticated token scope."
61204
+ ),
61205
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61206
+ thread_id: external_exports2.string().uuid().optional().describe(
61207
+ "Linzumi thread UUID. Defaults to the active coding job thread."
61208
+ ),
61209
+ goal: external_exports2.string().min(1).max(1e4).describe("Concrete user-visible goal for this coding job.")
61210
+ },
61211
+ async (params) => mcpJsonResult(
61212
+ await client.upsertCodingJobPlan(
61213
+ paramsWithDefaultThread(params, defaultThreadId)
61214
+ )
61215
+ )
61216
+ );
61217
+ server.tool(
61218
+ "linzumi_replace_coding_job_plan_steps",
61219
+ "Replace the ordered coding job plan steps. Use this for the initial plan and for substantial rewrites when the old plan no longer fits the work.",
61220
+ {
61221
+ workspace: external_exports2.string().optional().describe(
61222
+ "Workspace slug. Omit to use the authenticated token scope."
61223
+ ),
61224
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61225
+ thread_id: external_exports2.string().uuid().optional().describe(
61226
+ "Linzumi thread UUID. Defaults to the active coding job thread."
61227
+ ),
61228
+ goal: external_exports2.string().min(1).max(1e4).optional().describe(
61229
+ "Required when no plan exists yet; updates the current coding job goal."
61230
+ ),
61231
+ steps: external_exports2.array(
61232
+ external_exports2.object({
61233
+ title: external_exports2.string().min(1).max(255),
61234
+ description: external_exports2.string().min(1).max(1e4),
61235
+ status: external_exports2.enum([
61236
+ "pending",
61237
+ "active",
61238
+ "completed",
61239
+ "blocked",
61240
+ "canceled"
61241
+ ]),
61242
+ status_note: external_exports2.string().min(1).max(4e3).optional(),
61243
+ completed_debrief: external_exports2.string().min(1).max(2e4).optional()
61244
+ })
61245
+ ).min(1).max(50).describe(
61246
+ "Ordered user-visible plan steps. Completed steps must include completed_debrief."
61247
+ )
61248
+ },
61249
+ async (params) => mcpJsonResult(
61250
+ await client.replaceCodingJobPlanSteps(
61251
+ paramsWithDefaultThread(params, defaultThreadId)
61252
+ )
61253
+ )
61254
+ );
61255
+ server.tool(
61256
+ "linzumi_update_coding_job_plan_step",
61257
+ "Update one coding job plan step status or note. Read metadata first and pass the current step lock_version.",
61258
+ {
61259
+ workspace: external_exports2.string().optional().describe(
61260
+ "Workspace slug. Omit to use the authenticated token scope."
61261
+ ),
61262
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61263
+ thread_id: external_exports2.string().uuid().optional().describe(
61264
+ "Linzumi thread UUID. Defaults to the active coding job thread."
61265
+ ),
61266
+ step_id: external_exports2.string().uuid().describe("Step id returned by linzumi_get_coding_job_metadata."),
61267
+ lock_version: external_exports2.number().int().positive().describe("Current step lock_version from metadata."),
61268
+ status: external_exports2.enum([
61269
+ "pending",
61270
+ "active",
61271
+ "completed",
61272
+ "blocked",
61273
+ "canceled"
61274
+ ]),
61275
+ status_note: external_exports2.string().min(1).max(4e3).optional(),
61276
+ completed_debrief: external_exports2.string().min(1).max(2e4).optional().describe("Required when marking a step completed.")
61277
+ },
61278
+ async (params) => mcpJsonResult(
61279
+ await client.updateCodingJobPlanStep(
61280
+ paramsWithDefaultThread(params, defaultThreadId)
61281
+ )
61282
+ )
61283
+ );
61284
+ server.tool(
61285
+ "linzumi_link_coding_job_pull_request",
61286
+ "Link the active coding job to its primary GitHub pull request so PR comments, files, commits, checks, deployments, and reviews can flow into the metadata view.",
61287
+ {
61288
+ workspace: external_exports2.string().optional().describe(
61289
+ "Workspace slug. Omit to use the authenticated token scope."
61290
+ ),
61291
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61292
+ thread_id: external_exports2.string().uuid().optional().describe(
61293
+ "Linzumi thread UUID. Defaults to the active coding job thread."
61294
+ ),
61295
+ github_repo_owner: external_exports2.string().min(1).max(255),
61296
+ github_repo_name: external_exports2.string().min(1).max(255),
61297
+ github_pr_number: external_exports2.number().int().positive(),
61298
+ github_pr_url: external_exports2.string().url(),
61299
+ title: external_exports2.string().min(1).max(255),
61300
+ body: external_exports2.string().min(1).max(1e5).optional(),
61301
+ head_branch: external_exports2.string().min(1).max(255),
61302
+ head_sha: external_exports2.string().min(7).max(64),
61303
+ base_branch: external_exports2.string().min(1).max(255),
61304
+ base_sha: external_exports2.string().min(7).max(64),
61305
+ state: external_exports2.enum(["draft", "open", "closed", "merged"]).optional(),
61306
+ is_draft: external_exports2.boolean().optional(),
61307
+ review_status: external_exports2.enum(["unknown", "review_required", "approved", "changes_requested"]).optional(),
61308
+ source_remote_url: external_exports2.string().min(1).max(1e3).describe(
61309
+ "Git remote URL for the local job worktree; required when this job has no repo workspace yet."
61310
+ ),
61311
+ runtime_worktree_root: external_exports2.string().min(1).max(1e3).describe(
61312
+ "Absolute local worktree path for the coding job; required when this job has no repo workspace yet."
61313
+ ),
61314
+ dirty_status: external_exports2.enum(["unknown", "clean", "dirty"]).optional(),
61315
+ dirty_summary: external_exports2.string().min(1).max(2e3).optional()
61316
+ },
61317
+ async (params) => mcpJsonResult(
61318
+ await client.linkCodingJobPullRequest(
61319
+ paramsWithDefaultThread(params, defaultThreadId)
61320
+ )
61321
+ )
61322
+ );
61323
+ server.tool(
61324
+ "linzumi_upload_files",
61325
+ "Upload local files to Linzumi and attach them to a new, exact, or latest assistant message. Use silently; do not mention this tool in the visible reply.",
61326
+ {
61327
+ target: external_exports2.object({
61328
+ kind: external_exports2.enum(["new_message", "message_seq", "latest_assistant_message"]).describe(
61329
+ "new_message creates a message; message_seq edits an exact message; latest_assistant_message edits the latest assistant message in the scoped thread/channel."
61330
+ ),
61331
+ message_seq: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Required when kind is message_seq."),
61332
+ thread_id: external_exports2.string().optional().describe(
61333
+ "Thread UUID. Defaults to the active Linzumi thread when the MCP server was launched with one."
61334
+ )
61335
+ }).describe(
61336
+ "Attachment target. Do not guess; use new_message when there is no previous assistant message."
61337
+ ),
61338
+ files: external_exports2.array(external_exports2.string().min(1)).min(1).max(16).describe(
61339
+ "Local file paths to upload. Paths must be non-hidden supported file types inside the approved workspace or Downloads."
61340
+ ),
61341
+ body: external_exports2.string().min(1).max(2e4).optional().describe(
61342
+ "Required for new_message. Optional replacement body for message_seq/latest_assistant_message; when omitted, the existing message body is preserved."
61343
+ ),
61344
+ workspace: external_exports2.string().optional().describe(
61345
+ "Workspace slug. Omit to use the authenticated token scope."
61346
+ ),
61347
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope.")
61348
+ },
61349
+ async (params) => {
61350
+ const target = targetWithDefaultThread(
61351
+ params.target,
61352
+ defaultThreadId
61353
+ );
61354
+ const result = await uploadFilesWithClient({
61355
+ client,
61356
+ cwd,
61357
+ kandanUrl,
61358
+ target,
61359
+ files: params.files,
61360
+ workspace: params.workspace,
61361
+ channel: params.channel,
61362
+ body: params.body
61363
+ });
61364
+ return mcpJsonResult(result);
61365
+ }
61366
+ );
61367
+ server.tool(
61368
+ "linzumi_list_vault_secrets",
61369
+ "List vault secret names, descriptions, and whether each secret is personal or workspace-owned. Does not return secret values.",
61370
+ {},
61371
+ async (params) => mcpJsonResult(await client.listVaultSecrets(params))
61372
+ );
61373
+ }
61374
+ if (customEmojiMcpToolsEnabledForScope(toolScope)) {
61375
+ server.tool(
61376
+ "linzumi_upload_custom_emoji",
61377
+ "Upload a local image as a workspace custom emoji. For generated emoji art, create a transparent PNG with a simple centered subject, crisp edges, strong contrast, minimal tiny details, and enough outline/highlight separation to read at 16-24px on both dark and light backgrounds. Use lowercase shortcode names without colons unless preserving a user-provided :name:.",
61378
+ {
61379
+ name: customEmojiNameSchema.describe(
61380
+ "Custom emoji shortcode name, with or without surrounding colons."
61381
+ ),
61382
+ file: external_exports2.string().min(1).describe(
61383
+ "Local PNG/WebP/GIF/JPEG file path to upload. Prefer transparent PNG for generated emoji."
61384
+ ),
61385
+ workspace: external_exports2.string().optional().describe(
61386
+ "Workspace slug. Omit to use the authenticated token scope."
61387
+ )
61388
+ },
61389
+ async (params) => mcpJsonResult(
61390
+ await uploadCustomEmojiWithClient({
61391
+ client,
61392
+ cwd,
61393
+ kandanUrl,
61394
+ name: params.name,
61395
+ file: params.file,
61396
+ workspace: params.workspace
58777
61397
  })
58778
- );
58779
- }
58780
- );
58781
- server.tool(
58782
- "linzumi_send_channel_message",
58783
- "Send a plain-text message as the authenticated local-runner user to the scoped Linzumi channel. Requires channel.write.",
58784
- {
58785
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58786
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58787
- body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to post.")
58788
- },
58789
- async (params) => mcpJsonResult(await client.sendChannelMessage(params))
58790
- );
61398
+ )
61399
+ );
61400
+ server.tool(
61401
+ "linzumi_rename_custom_emoji",
61402
+ "Rename an existing workspace custom emoji. The new name may include surrounding colons; Linzumi stores the normalized shortcode without colons.",
61403
+ {
61404
+ emoji_id: external_exports2.union([external_exports2.string(), external_exports2.number().int().positive()]).describe(
61405
+ "Workspace custom emoji id returned by linzumi_upload_custom_emoji or workspace emoji listings."
61406
+ ),
61407
+ name: customEmojiNameSchema.describe(
61408
+ "New shortcode name, with or without surrounding colons."
61409
+ ),
61410
+ workspace: external_exports2.string().optional().describe(
61411
+ "Workspace slug. Omit to use the authenticated token scope."
61412
+ )
61413
+ },
61414
+ async (params) => mcpJsonResult(await client.renameCustomEmoji(params))
61415
+ );
61416
+ }
58791
61417
  server.tool(
58792
- "linzumi_send_thread_reply",
58793
- "Send a plain-text reply as the authenticated local-runner user to an existing thread in the scoped Linzumi channel. Requires thread.write.",
61418
+ "linzumi_note_project_directory",
61419
+ "Record one discovered local Git repository or worktree for onboarding import. Call once per project as soon as it is found.",
58794
61420
  {
58795
61421
  workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58796
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58797
- thread_id: external_exports2.string().uuid().describe("Existing Linzumi thread UUID."),
58798
- body: external_exports2.string().min(1).max(2e4).describe("Plain-text reply body to post.")
61422
+ runner_id: external_exports2.string().min(1).describe("Local runner/computer id provided in the discovery prompt."),
61423
+ path: external_exports2.string().min(1).describe("Absolute local path to the repository worktree root."),
61424
+ display_name: external_exports2.string().optional().describe("Short project label. Defaults to the path basename."),
61425
+ activity_at: external_exports2.string().optional().describe("Best-known recent activity time as an ISO-8601 timestamp."),
61426
+ import_key: external_exports2.string().optional().describe("Stable dedupe key. Defaults to path."),
61427
+ metadata: external_exports2.record(external_exports2.unknown()).optional().describe("Small structured metadata such as branch or remote origin.")
58799
61428
  },
58800
- async (params) => mcpJsonResult(await client.sendThreadReply(params))
61429
+ async (params) => mcpJsonResult(await client.noteProjectDirectory(params))
58801
61430
  );
58802
61431
  server.tool(
58803
- "linzumi_send_dm",
58804
- "Send a plain-text DM as the authenticated local-runner user to a visible user in the scoped workspace/channel. Requires dm.write.",
61432
+ "linzumi_note_agent_conversation",
61433
+ "Record one discovered Codex or Claude Code local conversation for onboarding import. Call once per conversation as soon as it is found.",
58805
61434
  {
58806
61435
  workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58807
- channel: external_exports2.string().optional().describe("Source channel slug used to verify target visibility."),
58808
- username: external_exports2.string().describe("Visible Linzumi username to DM."),
58809
- body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to DM.")
61436
+ runner_id: external_exports2.string().min(1).describe("Local runner/computer id provided in the discovery prompt."),
61437
+ source: external_exports2.enum(["codex", "claude_code"]).describe("Conversation source."),
61438
+ import_key: external_exports2.string().min(1).describe("Stable dedupe key, such as a session id or file path."),
61439
+ path: external_exports2.string().optional().describe("Absolute local path to the conversation file or directory."),
61440
+ display_name: external_exports2.string().optional().describe("Short label for the discovered conversation."),
61441
+ activity_at: external_exports2.string().optional().describe("Best-known recent activity time as an ISO-8601 timestamp."),
61442
+ metadata: external_exports2.record(external_exports2.unknown()).optional().describe(
61443
+ "Small structured metadata such as session id or project path."
61444
+ )
58810
61445
  },
58811
- async (params) => mcpJsonResult(await client.sendDm(params))
61446
+ async (params) => mcpJsonResult(await client.noteAgentConversation(params))
58812
61447
  );
58813
61448
  server.tool(
58814
- "linzumi_get_vault_values",
58815
- "Request values for explicit vault env var names after a thread-scoped human approval. Requires vault.read; thread_id resolves the approval thread.",
61449
+ "linzumi_note_onboarding_discovery_status",
61450
+ "Record onboarding discovery progress. Call when a search phase starts, checks another place, completes, or fails.",
58816
61451
  {
58817
61452
  workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58818
- channel: external_exports2.string().optional().describe(
58819
- "Channel slug used to disambiguate the approval thread when needed."
61453
+ runner_id: external_exports2.string().min(1).describe("Local runner/computer id provided in the discovery prompt."),
61454
+ phase: external_exports2.enum(["projects", "conversations"]).describe("Discovery phase being reported."),
61455
+ status: external_exports2.enum(["starting", "running", "completed", "failed"]).describe("Current phase status."),
61456
+ places_searched: external_exports2.number().int().nonnegative().optional().describe(
61457
+ "Count of concrete folders/files/known locations checked so far."
58820
61458
  ),
58821
- thread_id: external_exports2.string().uuid().describe("Existing Linzumi thread UUID for the inline approval."),
58822
- env_var_names: external_exports2.array(external_exports2.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/)).min(1).max(20).describe(
58823
- "Explicit vault env var names to request. This tool cannot list all vault keys."
61459
+ message: external_exports2.string().optional().describe("Short user-facing status note."),
61460
+ current_place: external_exports2.string().optional().describe(
61461
+ "Current folder, file, Git command target, or known location being checked."
58824
61462
  ),
58825
- description: external_exports2.string().min(1).max(500).describe("Human-readable reason shown in the approval request."),
58826
- justification: external_exports2.string().min(10).max(1e3).describe("Agent justification shown to the approver.")
61463
+ current_source: external_exports2.string().optional().describe(
61464
+ "Short machine-readable source label such as git, codex, or claude_code."
61465
+ ),
61466
+ last_error: external_exports2.string().optional().describe("Short failure detail when status is failed."),
61467
+ metadata: external_exports2.record(external_exports2.unknown()).optional().describe("Small structured status metadata.")
58827
61468
  },
58828
- async (params) => mcpJsonResult(await client.getVaultValues(params))
61469
+ async (params) => mcpJsonResult(
61470
+ await client.noteOnboardingDiscoveryStatus(params)
61471
+ )
58829
61472
  );
61473
+ if (toolScope === "all") {
61474
+ server.tool(
61475
+ "linzumi_dm_owner",
61476
+ "Send a plain-text DM to the configured Commander owner. Requires dm.write and a visible owner username.",
61477
+ {
61478
+ owner_username: external_exports2.string().optional().describe(
61479
+ "Owner username. Defaults to LINZUMI_MCP_OWNER_USERNAME or --owner-username."
61480
+ ),
61481
+ workspace: external_exports2.string().optional().describe(
61482
+ "Workspace slug. Omit to use the authenticated token scope."
61483
+ ),
61484
+ channel: external_exports2.string().optional().describe("Source channel slug used to verify owner visibility."),
61485
+ body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to DM.")
61486
+ },
61487
+ async (params) => {
61488
+ const owner = params.owner_username ?? ownerUsername;
61489
+ if (owner === void 0) {
61490
+ throw new Error("owner_username is required for linzumi_dm_owner");
61491
+ }
61492
+ return mcpJsonResult(
61493
+ await client.dmOwner({
61494
+ ...params,
61495
+ owner_username: owner
61496
+ })
61497
+ );
61498
+ }
61499
+ );
61500
+ server.tool(
61501
+ "linzumi_send_channel_message",
61502
+ "Send a plain-text message as the authenticated local-runner user to the scoped Linzumi channel. Requires channel.write.",
61503
+ {
61504
+ workspace: external_exports2.string().optional().describe(
61505
+ "Workspace slug. Omit to use the authenticated token scope."
61506
+ ),
61507
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61508
+ body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to post.")
61509
+ },
61510
+ async (params) => mcpJsonResult(await client.sendChannelMessage(params))
61511
+ );
61512
+ server.tool(
61513
+ "linzumi_send_thread_reply",
61514
+ "Send a plain-text reply as the authenticated local-runner user to an existing thread in the scoped Linzumi channel. Requires thread.write.",
61515
+ {
61516
+ workspace: external_exports2.string().optional().describe(
61517
+ "Workspace slug. Omit to use the authenticated token scope."
61518
+ ),
61519
+ channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
61520
+ thread_id: external_exports2.string().uuid().describe("Existing Linzumi thread UUID."),
61521
+ body: external_exports2.string().min(1).max(2e4).describe("Plain-text reply body to post.")
61522
+ },
61523
+ async (params) => mcpJsonResult(await client.sendThreadReply(params))
61524
+ );
61525
+ server.tool(
61526
+ "linzumi_send_dm",
61527
+ "Send a plain-text DM as the authenticated local-runner user to a visible user in the scoped workspace/channel. Requires dm.write.",
61528
+ {
61529
+ workspace: external_exports2.string().optional().describe(
61530
+ "Workspace slug. Omit to use the authenticated token scope."
61531
+ ),
61532
+ channel: external_exports2.string().optional().describe("Source channel slug used to verify target visibility."),
61533
+ username: external_exports2.string().describe("Visible Linzumi username to DM."),
61534
+ body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to DM.")
61535
+ },
61536
+ async (params) => mcpJsonResult(await client.sendDm(params))
61537
+ );
61538
+ server.tool(
61539
+ "linzumi_get_vault_values",
61540
+ "Request values for explicit vault env var names after a thread-scoped human approval. Requires vault.read; thread_id resolves the approval thread.",
61541
+ {
61542
+ workspace: external_exports2.string().optional().describe(
61543
+ "Workspace slug. Omit to use the authenticated token scope."
61544
+ ),
61545
+ channel: external_exports2.string().optional().describe(
61546
+ "Channel slug used to disambiguate the approval thread when needed."
61547
+ ),
61548
+ thread_id: external_exports2.string().uuid().describe("Existing Linzumi thread UUID for the inline approval."),
61549
+ env_var_names: external_exports2.array(external_exports2.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/)).min(1).max(20).describe(
61550
+ "Explicit vault env var names to request. This tool cannot list all vault keys."
61551
+ ),
61552
+ description: external_exports2.string().min(1).max(500).describe("Human-readable reason shown in the approval request."),
61553
+ justification: external_exports2.string().min(10).max(1e3).describe("Agent justification shown to the approver.")
61554
+ },
61555
+ async (params) => mcpJsonResult(await client.getVaultValues(params))
61556
+ );
61557
+ }
58830
61558
  await server.connect(new StdioServerTransport());
58831
61559
  }
58832
61560
  async function uploadFilesWithClient(args) {
@@ -58854,13 +61582,13 @@ async function uploadFilesWithClient(args) {
58854
61582
  if (uploadUrl === void 0) {
58855
61583
  throw new Error("Linzumi upload prepare response missing upload_url");
58856
61584
  }
58857
- const bytes = await readFile2(file.path);
61585
+ const bytes = await readFile3(file.path);
58858
61586
  const uploadBody = bytes.buffer.slice(
58859
61587
  bytes.byteOffset,
58860
61588
  bytes.byteOffset + bytes.byteLength
58861
61589
  );
58862
61590
  const response = await fetch(
58863
- resolveLinzumiUploadUrl(args.kandanUrl, uploadUrl),
61591
+ resolveLinzumiUploadUrl2(args.kandanUrl, uploadUrl),
58864
61592
  {
58865
61593
  method: uploadMethod,
58866
61594
  headers: { "content-type": file.contentType },
@@ -58904,13 +61632,13 @@ async function uploadCustomEmojiWithClient(args) {
58904
61632
  if (uploadUrl === void 0) {
58905
61633
  throw new Error("Linzumi custom emoji prepare response missing upload_url");
58906
61634
  }
58907
- const bytes = await readFile2(file.path);
61635
+ const bytes = await readFile3(file.path);
58908
61636
  const uploadBody = bytes.buffer.slice(
58909
61637
  bytes.byteOffset,
58910
61638
  bytes.byteOffset + bytes.byteLength
58911
61639
  );
58912
61640
  const response = await fetch(
58913
- resolveLinzumiUploadUrl(args.kandanUrl, uploadUrl),
61641
+ resolveLinzumiUploadUrl2(args.kandanUrl, uploadUrl),
58914
61642
  {
58915
61643
  method: uploadMethod,
58916
61644
  headers: { "content-type": file.contentType },
@@ -58966,7 +61694,14 @@ function targetWithDefaultThread(target, defaultThreadId) {
58966
61694
  return target;
58967
61695
  }
58968
61696
  }
58969
- function resolveLinzumiUploadUrl(kandanUrl, uploadUrl) {
61697
+ function paramsWithDefaultThread(params, defaultThreadId) {
61698
+ const existingThreadId = stringValue(params.thread_id);
61699
+ if (existingThreadId !== void 0 || defaultThreadId === void 0) {
61700
+ return params;
61701
+ }
61702
+ return { ...params, thread_id: defaultThreadId };
61703
+ }
61704
+ function resolveLinzumiUploadUrl2(kandanUrl, uploadUrl) {
58970
61705
  try {
58971
61706
  return new URL(uploadUrl).toString();
58972
61707
  } catch (_error) {
@@ -59004,7 +61739,8 @@ async function runMcpConfig(args) {
59004
61739
  accessToken: token,
59005
61740
  delegationAuthFilePath: stringValue5(values, "delegation-auth-file"),
59006
61741
  ownerUsername: stringValue5(values, "owner-username") ?? process.env.LINZUMI_MCP_OWNER_USERNAME,
59007
- operatingMode
61742
+ operatingMode,
61743
+ toolScope: mcpToolScope(stringValue5(values, "tool-scope"))
59008
61744
  });
59009
61745
  switch (format) {
59010
61746
  case "codex":
@@ -59132,6 +61868,17 @@ function mcpOperatingMode(value) {
59132
61868
  throw new Error("--mode must be voice or text");
59133
61869
  }
59134
61870
  }
61871
+ function mcpToolScope(value) {
61872
+ switch (value) {
61873
+ case void 0:
61874
+ return "all";
61875
+ case "all":
61876
+ case "onboarding-discovery":
61877
+ return value;
61878
+ default:
61879
+ throw new Error("--tool-scope must be all or onboarding-discovery");
61880
+ }
61881
+ }
59135
61882
  function required(values, key) {
59136
61883
  const value = stringValue5(values, key);
59137
61884
  if (value === void 0) {
@@ -59474,6 +62221,7 @@ function optionalObjectField(input, key) {
59474
62221
 
59475
62222
  // src/index.ts
59476
62223
  init_userFacingErrors();
62224
+ var onboardingDiscoveryRequiredScopes = ["local_runner.discovery.write"];
59477
62225
  var flagDefinitions = /* @__PURE__ */ new Map([
59478
62226
  ["version", { kind: "boolean" }],
59479
62227
  ["api-url", { kind: "value" }],
@@ -59497,6 +62245,7 @@ var flagDefinitions = /* @__PURE__ */ new Map([
59497
62245
  ["forward-port", { kind: "value" }],
59498
62246
  ["code-server-bin", { kind: "value" }],
59499
62247
  ["fast", { kind: "boolean" }],
62248
+ ["no-onboarding-discovery", { kind: "boolean" }],
59500
62249
  ["log-file", { kind: "value" }],
59501
62250
  ["status-dir", { kind: "value" }],
59502
62251
  ["timeout-ms", { kind: "value" }],
@@ -59566,6 +62315,7 @@ async function main(args) {
59566
62315
  await runSignupFlow2({
59567
62316
  forceSignup: options.forceSignup,
59568
62317
  serviceUrl: options.serviceUrl,
62318
+ codeServerBin: options.codeServerBin,
59569
62319
  openBrowser: false,
59570
62320
  debugLaunchPayload: options.debugLaunchPayload,
59571
62321
  signupServerClient: false
@@ -59584,6 +62334,7 @@ async function main(args) {
59584
62334
  await runSignupFlow2({
59585
62335
  forceSignup: options.forceSignup,
59586
62336
  serviceUrl: options.serviceUrl,
62337
+ codeServerBin: options.codeServerBin,
59587
62338
  openBrowser: options.openBrowser,
59588
62339
  debugLaunchPayload: options.debugLaunchPayload
59589
62340
  });
@@ -59617,8 +62368,41 @@ async function main(args) {
59617
62368
  await runThreadCodexWorker(parseThreadCodexWorkerArgs(parsed.args));
59618
62369
  return;
59619
62370
  }
59620
- const options = await parseRunnerArgs(parsed.args);
59621
- await runLocalCodexRunner(withLocalMachineId(options));
62371
+ const telemetryApiUrl = peekFlagValue(parsed.args, [
62372
+ "--api-url",
62373
+ "--linzumi-url"
62374
+ ]);
62375
+ if (!isHelpOrVersionInvocation(parsed.args)) {
62376
+ void postLifecycleEvent(
62377
+ {
62378
+ name: "commander.connect.started",
62379
+ attributes: {
62380
+ ...workspaceTelemetryAttribute(
62381
+ peekFlagValue(parsed.args, ["--workspace"])
62382
+ ),
62383
+ launch_source: connectLaunchSourceTelemetryValue()
62384
+ }
62385
+ },
62386
+ { apiUrl: telemetryApiUrl }
62387
+ );
62388
+ }
62389
+ try {
62390
+ const options = await parseRunnerArgs(parsed.args);
62391
+ await runLocalCodexRunner(withLocalMachineId(options));
62392
+ } catch (error) {
62393
+ await postLifecycleEvent(
62394
+ {
62395
+ name: "commander.error",
62396
+ severity: "ERROR",
62397
+ attributes: {
62398
+ ...lifecycleErrorAttributes(error),
62399
+ launch_source: connectLaunchSourceTelemetryValue()
62400
+ }
62401
+ },
62402
+ { apiUrl: telemetryApiUrl }
62403
+ );
62404
+ throw error;
62405
+ }
59622
62406
  return;
59623
62407
  }
59624
62408
  }
@@ -59954,6 +62738,7 @@ async function parseStartRunnerArgs(args, deps = {
59954
62738
  const allowedCwds = Array.from(/* @__PURE__ */ new Set([cwd, ...explicitAllowedCwds]));
59955
62739
  const requestedCodexBin = stringValue7(values, "codex-bin") ?? "codex";
59956
62740
  const customCodeServerBin = stringValue7(values, "code-server-bin");
62741
+ const onboardingDiscovery = values.get("no-onboarding-discovery") === true ? void 0 : "start";
59957
62742
  const initialDependencyStatus = await deps.buildDependencyStatus({
59958
62743
  cwd,
59959
62744
  codexBin: requestedCodexBin,
@@ -59972,9 +62757,10 @@ async function parseStartRunnerArgs(args, deps = {
59972
62757
  const token = await deps.resolveToken({
59973
62758
  kandanUrl,
59974
62759
  explicitToken,
59975
- onboarding: "start",
62760
+ onboarding: onboardingDiscovery === "start" ? "start" : void 0,
59976
62761
  authFilePath,
59977
62762
  callbackHost,
62763
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0,
59978
62764
  reportRejectedCachedToken
59979
62765
  });
59980
62766
  const target = await deps.fetchStartTarget({ kandanUrl, accessToken: token });
@@ -59982,7 +62768,8 @@ async function parseStartRunnerArgs(args, deps = {
59982
62768
  kandanUrl,
59983
62769
  accessToken: token,
59984
62770
  workspaceSlug: target.workspaceSlug,
59985
- channelSlug: target.channelSlug
62771
+ channelSlug: target.channelSlug,
62772
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0
59986
62773
  });
59987
62774
  const targetToken = tokenMatchesTarget ? token : await resolveStartTargetToken({
59988
62775
  kandanUrl,
@@ -59990,6 +62777,7 @@ async function parseStartRunnerArgs(args, deps = {
59990
62777
  target,
59991
62778
  authFilePath,
59992
62779
  callbackHost,
62780
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0,
59993
62781
  resolveToken: deps.resolveToken,
59994
62782
  reportRejectedCachedToken
59995
62783
  });
@@ -60031,6 +62819,7 @@ async function parseStartRunnerArgs(args, deps = {
60031
62819
  dependencyStatus,
60032
62820
  claudeCodeAvailable,
60033
62821
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
62822
+ onboardingDiscovery,
60034
62823
  channelSession: {
60035
62824
  workspaceSlug: target.workspaceSlug,
60036
62825
  channelSlug: target.channelSlug,
@@ -60140,7 +62929,7 @@ async function parseAgentRunnerArgs(args, deps = {
60140
62929
  };
60141
62930
  }
60142
62931
  function readAgentTokenTextFile(path2) {
60143
- return existsSync14(path2) ? readFileSync16(path2, "utf8") : void 0;
62932
+ return existsSync16(path2) ? readFileSync19(path2, "utf8") : void 0;
60144
62933
  }
60145
62934
  function rejectAgentRunnerTargetingFlags(values) {
60146
62935
  const unsupportedFlags = [
@@ -60212,6 +63001,7 @@ async function resolveStartTargetToken(args) {
60212
63001
  channelSlug: args.target.channelSlug,
60213
63002
  authFilePath: args.authFilePath,
60214
63003
  callbackHost: args.callbackHost,
63004
+ requiredScopes: args.requiredScopes,
60215
63005
  reportRejectedCachedToken: args.reportRejectedCachedToken
60216
63006
  });
60217
63007
  }
@@ -60251,6 +63041,7 @@ async function parseRunnerArgs(args, deps = {
60251
63041
  ) : [...localConfiguredAllowedCwds.allowedCwds];
60252
63042
  const requestedCodexBin = stringValue7(values, "codex-bin") ?? "codex";
60253
63043
  const customCodeServerBin = stringValue7(values, "code-server-bin");
63044
+ const onboardingDiscovery = values.get("no-onboarding-discovery") === true ? void 0 : "start";
60254
63045
  const initialDependencyStatus = await deps.buildDependencyStatus({
60255
63046
  cwd,
60256
63047
  codexBin: requestedCodexBin,
@@ -60265,6 +63056,7 @@ async function parseRunnerArgs(args, deps = {
60265
63056
  workspaceSlug,
60266
63057
  authFilePath: stringValue7(values, "auth-file"),
60267
63058
  callbackHost: stringValue7(values, "oauth-callback-host"),
63059
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0,
60268
63060
  reportRejectedCachedToken: () => {
60269
63061
  process.stderr.write(
60270
63062
  "Cached Linzumi local runner auth was rejected; starting OAuth.\n"
@@ -60312,6 +63104,7 @@ async function parseRunnerArgs(args, deps = {
60312
63104
  claudeCodeAvailable,
60313
63105
  workspaceSlug: workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
60314
63106
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
63107
+ onboardingDiscovery,
60315
63108
  channelSession: void 0
60316
63109
  };
60317
63110
  }
@@ -60327,6 +63120,15 @@ function runnerRuntimeDefaultsFromValues(values) {
60327
63120
  function electronAutoConnectLaunchSource() {
60328
63121
  return process.env.LINZUMI_ELECTRON_AUTO_CONNECT_RUNNER === "1" ? "electron_auto_connect" : void 0;
60329
63122
  }
63123
+ function connectLaunchSourceTelemetryValue() {
63124
+ return electronAutoConnectLaunchSource() ?? "cli";
63125
+ }
63126
+ function workspaceTelemetryAttribute(workspace) {
63127
+ return workspace === void 0 ? {} : { workspace };
63128
+ }
63129
+ function isHelpOrVersionInvocation(args) {
63130
+ return args.some((arg) => ["--help", "-h", "--version"].includes(arg));
63131
+ }
60330
63132
  function kandanUrlValue(values) {
60331
63133
  const apiUrl = stringValue7(values, "api-url");
60332
63134
  const linzumiUrl = stringValue7(values, "linzumi-url");
@@ -60438,10 +63240,10 @@ function rejectStartTargetingFlags(values) {
60438
63240
  }
60439
63241
  function resolveUserPath(pathValue) {
60440
63242
  if (pathValue === "~") {
60441
- return homedir13();
63243
+ return homedir15();
60442
63244
  }
60443
63245
  if (pathValue.startsWith("~/")) {
60444
- return resolve10(homedir13(), pathValue.slice(2));
63246
+ return resolve10(homedir15(), pathValue.slice(2));
60445
63247
  }
60446
63248
  return resolve10(pathValue);
60447
63249
  }
@@ -60637,7 +63439,7 @@ Codex:
60637
63439
  --approval-policy <value> Approval-policy metadata shown in Linzumi
60638
63440
  --stream-flush-ms <ms> Batch live Codex deltas before Linzumi persistence, default 150
60639
63441
  --fast Mark this runner as low-latency/fast in the availability message
60640
- --log-file <path> JSONL event log path, default ~/.linzumi/logs/runner-events.jsonl
63442
+ --log-file <path> JSONL event log path, default ~/.linzumi/logs/linzumi-runner.log
60641
63443
  --allowed-cwd <paths> Extra comma-separated roots where Linzumi may start local Codex sessions
60642
63444
  --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as authenticated previews
60643
63445
  --code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from Linzumi.
@@ -60719,7 +63521,7 @@ What it does:
60719
63521
  Options:
60720
63522
  --api-url <url> Linzumi API URL used to select the stored scoped runner id
60721
63523
  --status-dir <path> Status directory, default ~/.linzumi/commanders
60722
- --log-file <path> Commander log path, default in the status dir
63524
+ --log-file <path> Commander log path, default ~/.linzumi/logs/<runner-id>.log
60723
63525
  --timeout-ms <ms> Wait timeout, default 30000
60724
63526
 
60725
63527
  All normal Commander options such as --agent-token-file, --allowed-cwd,