@linzumi/cli 0.0.77-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 +3196 -563
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9772,7 +9772,8 @@ function linzumiMcpServerConfig(options) {
9772
9772
  ...options.cwd === void 0 ? [] : ["--cwd", options.cwd],
9773
9773
  ...options.threadId === void 0 ? [] : ["--thread-id", options.threadId],
9774
9774
  "--mode",
9775
- options.operatingMode ?? "text"
9775
+ options.operatingMode ?? "text",
9776
+ ...options.toolScope === void 0 || options.toolScope === "all" ? [] : ["--tool-scope", options.toolScope]
9776
9777
  ],
9777
9778
  env: {
9778
9779
  ...options.accessToken === void 0 ? {} : { LINZUMI_MCP_ACCESS_TOKEN: options.accessToken }
@@ -10725,6 +10726,9 @@ async function validateLocalRunnerToken(args) {
10725
10726
  if (args.channelSlug !== void 0) {
10726
10727
  url.searchParams.set("channel", args.channelSlug);
10727
10728
  }
10729
+ for (const scope of args.requiredScopes ?? []) {
10730
+ url.searchParams.append("required_scope", scope);
10731
+ }
10728
10732
  const response = await fetch(url, {
10729
10733
  method: "GET",
10730
10734
  headers: { authorization: `Bearer ${args.accessToken}` }
@@ -13577,7 +13581,7 @@ async function resolveEditorRuntime(options) {
13577
13581
  fetchImpl: options.fetchImpl ?? fetch
13578
13582
  });
13579
13583
  if (!manifest.ok) {
13580
- return unavailable("manifest_unavailable");
13584
+ return unavailable(manifest.reason);
13581
13585
  }
13582
13586
  const cacheRoot = options.cacheRoot ?? defaultEditorRuntimeCacheRoot();
13583
13587
  const installed = installedRuntime(cacheRoot, manifest.manifest);
@@ -13669,17 +13673,24 @@ async function fetchApprovedManifest(args) {
13669
13673
  headers: { authorization: `Bearer ${args.token}` }
13670
13674
  }).catch(() => void 0);
13671
13675
  if (response === void 0) {
13672
- return { ok: false };
13676
+ return { ok: false, reason: "manifest_unavailable" };
13673
13677
  }
13674
13678
  if (response.status !== 200) {
13675
- return { ok: false };
13679
+ return {
13680
+ ok: false,
13681
+ reason: response.status === 401 ? await unauthorizedManifestReason(response) : "manifest_unavailable"
13682
+ };
13676
13683
  }
13677
13684
  const body = await response.json();
13678
13685
  if (!isJsonObject(body) || body.ok !== true || !isJsonObject(body.runtime)) {
13679
- return { ok: false };
13686
+ return { ok: false, reason: "manifest_unavailable" };
13680
13687
  }
13681
13688
  return normalizeManifest(body.runtime);
13682
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
+ }
13683
13694
  function normalizeManifest(value) {
13684
13695
  const version = nonEmptyString(value.version);
13685
13696
  const platform = nonEmptyString(value.platform);
@@ -13690,7 +13701,7 @@ function normalizeManifest(value) {
13690
13701
  const manifestPath = nonEmptyString(value.manifestPath) ?? "linzumi-editor-runtime.json";
13691
13702
  const assets = normalizeRuntimeAssets(value.assets);
13692
13703
  if (version === void 0 || platform === void 0 || archiveUrl === void 0 || archiveSha256 === void 0 || codeServerVersion === void 0 || assets === void 0) {
13693
- return { ok: false };
13704
+ return { ok: false, reason: "manifest_unavailable" };
13694
13705
  }
13695
13706
  return {
13696
13707
  ok: true,
@@ -14264,6 +14275,20 @@ function assertRunnerConnectionDependencies(status) {
14264
14275
  );
14265
14276
  }
14266
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
+ }
14267
14292
  throw new Error(
14268
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."
14269
14294
  );
@@ -14632,7 +14657,7 @@ var linzumiCliVersion, linzumiCliVersionText;
14632
14657
  var init_version = __esm({
14633
14658
  "src/version.ts"() {
14634
14659
  "use strict";
14635
- linzumiCliVersion = "0.0.77-beta";
14660
+ linzumiCliVersion = "0.0.78-beta";
14636
14661
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
14637
14662
  }
14638
14663
  });
@@ -14938,6 +14963,39 @@ function updateRunnerConsoleDashboard(state, event, payload, nowMs) {
14938
14963
  state.lastUpdateAtMs = nowMs;
14939
14964
  const previousJobKey = latestDashboardJob(state)?.key;
14940
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
+ }
14941
14999
  case "kandan.message_queued":
14942
15000
  case "codex.turn_starting": {
14943
15001
  const job = dashboardJobForPayload(state, payload, nowMs);
@@ -15006,12 +15064,15 @@ function updateRunnerConsoleDashboard(state, event, payload, nowMs) {
15006
15064
  rememberRawLine(state, event, payload, previousJobKey);
15007
15065
  }
15008
15066
  function renderRunnerConsoleDashboard(state, nowMs) {
15009
- 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) {
15010
15068
  return void 0;
15011
15069
  }
15012
15070
  const jobs = dashboardJobs(state);
15013
15071
  switch (state.mode.type) {
15014
15072
  case "table":
15073
+ if (state.jobs.size === 0 && state.discovery.size > 0) {
15074
+ return renderSignupDiscoveryWelcome(state, nowMs);
15075
+ }
15015
15076
  return renderDashboardTable(state, jobs, nowMs);
15016
15077
  case "raw_all":
15017
15078
  return renderRawDashboard(
@@ -15037,9 +15098,195 @@ function renderRunnerConsoleDashboard(state, nowMs) {
15037
15098
  }
15038
15099
  }
15039
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
+ }
15040
15285
  function renderDashboardTable(state, jobs, nowMs) {
15041
15286
  const model = dashboardTableModel(state, jobs, nowMs);
15042
15287
  return [
15288
+ runnerWelcomeHeader(),
15289
+ "",
15043
15290
  "Linzumi Commander",
15044
15291
  "",
15045
15292
  `Jobs: ${jobs.length} Last update: ${timeAgo(state.lastUpdateAtMs, nowMs)}`,
@@ -15052,6 +15299,9 @@ function renderDashboardTable(state, jobs, nowMs) {
15052
15299
  ""
15053
15300
  ].join("\n");
15054
15301
  }
15302
+ function runnerWelcomeHeader() {
15303
+ return runnerWelcomeHeaderLines.join("\n");
15304
+ }
15055
15305
  function creditExhaustionBanner(summary) {
15056
15306
  if (summary === void 0) {
15057
15307
  return [];
@@ -15060,7 +15310,12 @@ function creditExhaustionBanner(summary) {
15060
15310
  }
15061
15311
  function dashboardTableModel(state, jobs, nowMs) {
15062
15312
  const selectedJobKey = selectedDashboardJobKey(state, jobs);
15063
- 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()] : [
15064
15319
  dashboardTableHeader(),
15065
15320
  ...jobs.map((job) => jobRow(job, selectedJobKey, nowMs))
15066
15321
  ];
@@ -15070,6 +15325,91 @@ function dashboardTableModel(state, jobs, nowMs) {
15070
15325
  rows
15071
15326
  };
15072
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
+ }
15073
15413
  function dashboardTableHeader() {
15074
15414
  return [
15075
15415
  "",
@@ -15114,6 +15454,10 @@ function formatRunnerConsoleEvent(event, payload) {
15114
15454
  return ignoredMessage(payload);
15115
15455
  case "kandan.message_queued":
15116
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);
15117
15461
  case "kandan.chat_event_failed":
15118
15462
  return `Incoming message handling failed: seq=${text(payload.seq)} reason=${text(payload.message)}`;
15119
15463
  case "kandan.reconnected":
@@ -15153,6 +15497,20 @@ function formatRunnerConsoleEvent(event, payload) {
15153
15497
  return void 0;
15154
15498
  }
15155
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
+ }
15156
15514
  function connectedRunnerMessage(payload) {
15157
15515
  return [
15158
15516
  "Connected to Linzumi",
@@ -15440,11 +15798,11 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
15440
15798
  top: 0,
15441
15799
  left: 0,
15442
15800
  width: "100%",
15443
- height: 6,
15801
+ height: 1,
15444
15802
  tags: false
15445
15803
  });
15446
15804
  const tableElement = blessed.listtable({
15447
- top: 6,
15805
+ top: 1,
15448
15806
  left: 0,
15449
15807
  width: "100%",
15450
15808
  bottom: 1,
@@ -15566,7 +15924,15 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
15566
15924
  switch (state.mode.type) {
15567
15925
  case "table": {
15568
15926
  const model = dashboardTableModel(state, dashboardJobs(state), nowMs);
15569
- 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);
15570
15936
  header.show();
15571
15937
  tableElement.setData(model.rows);
15572
15938
  tableElement.show();
@@ -15617,6 +15983,9 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
15617
15983
  }
15618
15984
  return { render, destroy };
15619
15985
  }
15986
+ function tuiScreenHeight(screen) {
15987
+ return typeof screen.height === "number" ? screen.height : 24;
15988
+ }
15620
15989
  function syncTuiTableSelection(tableElement, model) {
15621
15990
  const selectedIndex = model.jobs.findIndex(
15622
15991
  (job) => job.key === model.selectedJobKey
@@ -15624,8 +15993,46 @@ function syncTuiTableSelection(tableElement, model) {
15624
15993
  const tableIndex = selectedIndex < 0 ? 1 : selectedIndex + 1;
15625
15994
  tableElement.select(tableIndex);
15626
15995
  }
15627
- 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
+ }
15628
16033
  return [
16034
+ welcomeHeader,
16035
+ "",
15629
16036
  "Linzumi Commander",
15630
16037
  `Jobs: ${jobCount} Last update: ${timeAgo(state.lastUpdateAtMs, nowMs)}`,
15631
16038
  ...creditExhaustionBanner(state.creditExhaustion),
@@ -15634,6 +16041,20 @@ function tuiHeaderContent(state, jobCount, nowMs) {
15634
16041
  "Controls: click row/enter raw job | r raw stream | esc/back table | mouse wheel scroll"
15635
16042
  ].join("\n");
15636
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
+ }
15637
16058
  function updateRunnerConsoleDashboardMode(state, key) {
15638
16059
  switch (key) {
15639
16060
  case "r":
@@ -15887,12 +16308,13 @@ function stringValue4(value) {
15887
16308
  function numberValue(value) {
15888
16309
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
15889
16310
  }
15890
- 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;
15891
16312
  var init_runnerConsoleReporter = __esm({
15892
16313
  "src/runnerConsoleReporter.ts"() {
15893
16314
  "use strict";
15894
16315
  dashboardState = {
15895
16316
  jobs: /* @__PURE__ */ new Map(),
16317
+ discovery: /* @__PURE__ */ new Map(),
15896
16318
  rawLines: [],
15897
16319
  nextJobOrdinal: 0,
15898
16320
  mode: { type: "table" },
@@ -15900,7 +16322,10 @@ var init_runnerConsoleReporter = __esm({
15900
16322
  tokenUsage: void 0,
15901
16323
  rateLimit: void 0,
15902
16324
  creditExhaustion: void 0,
15903
- lastUpdateAtMs: void 0
16325
+ lastUpdateAtMs: void 0,
16326
+ browserUrl: void 0,
16327
+ workspace: void 0,
16328
+ runnerId: void 0
15904
16329
  };
15905
16330
  maxRawLines = 500;
15906
16331
  escapeKey = "\x1B";
@@ -15919,6 +16344,37 @@ var init_runnerConsoleReporter = __esm({
15919
16344
  { width: 12 },
15920
16345
  { width: 24 }
15921
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;
15922
16378
  redrawScreen = (screen) => {
15923
16379
  process.stdout.write(`\x1B[2J\x1B[H${screen}`);
15924
16380
  };
@@ -15931,18 +16387,1052 @@ var init_runnerConsoleReporter = __esm({
15931
16387
  }
15932
16388
  });
15933
16389
 
15934
- // src/authCache.ts
15935
- 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";
15936
16642
  import { homedir as homedir9 } from "node:os";
15937
- 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";
15938
17428
  function defaultAuthFilePath() {
15939
- return join12(homedir9(), ".linzumi", "auth.json");
17429
+ return join15(homedir11(), ".linzumi", "auth.json");
15940
17430
  }
15941
17431
  function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePath()) {
15942
- if (!existsSync8(authFilePath)) {
17432
+ if (!existsSync10(authFilePath)) {
15943
17433
  return void 0;
15944
17434
  }
15945
- const authFile = parseAuthFile(readFileSync9(authFilePath, "utf8"));
17435
+ const authFile = parseAuthFile(readFileSync12(authFilePath, "utf8"));
15946
17436
  const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
15947
17437
  const entry = authFile.local_codex_runner?.[kandanBaseUrl];
15948
17438
  if (entry === void 0 || entry.access_token.trim() === "") {
@@ -15959,12 +17449,12 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
15959
17449
  };
15960
17450
  }
15961
17451
  function readPersonalAgentDelegationToken(authFilePath) {
15962
- if (!existsSync8(authFilePath)) {
17452
+ if (!existsSync10(authFilePath)) {
15963
17453
  throw new Error(
15964
17454
  `missing personal-agent delegation auth file: ${authFilePath}`
15965
17455
  );
15966
17456
  }
15967
- const authFile = parseAuthFile(readFileSync9(authFilePath, "utf8"));
17457
+ const authFile = parseAuthFile(readFileSync12(authFilePath, "utf8"));
15968
17458
  const entry = authFile.personal_agent_delegation;
15969
17459
  if (entry === void 0 || entry.access_token.trim() === "") {
15970
17460
  throw new Error(
@@ -15982,7 +17472,7 @@ function readPersonalAgentDelegationToken(authFilePath) {
15982
17472
  }
15983
17473
  function writeCachedLocalRunnerToken(args) {
15984
17474
  const authFilePath = args.authFilePath ?? defaultAuthFilePath();
15985
- const existing = existsSync8(authFilePath) ? parseAuthFile(readFileSync9(authFilePath, "utf8")) : { version: 1 };
17475
+ const existing = existsSync10(authFilePath) ? parseAuthFile(readFileSync12(authFilePath, "utf8")) : { version: 1 };
15986
17476
  const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
15987
17477
  const issuedAt = /* @__PURE__ */ new Date();
15988
17478
  const expiresAt = args.expiresInSeconds === void 0 ? void 0 : new Date(
@@ -16000,8 +17490,8 @@ function writeCachedLocalRunnerToken(args) {
16000
17490
  }
16001
17491
  }
16002
17492
  };
16003
- mkdirSync8(dirname8(authFilePath), { recursive: true });
16004
- writeFileSync6(authFilePath, `${JSON.stringify(next, null, 2)}
17493
+ mkdirSync9(dirname10(authFilePath), { recursive: true });
17494
+ writeFileSync7(authFilePath, `${JSON.stringify(next, null, 2)}
16005
17495
  `, "utf8");
16006
17496
  return {
16007
17497
  accessToken: args.accessToken,
@@ -16419,9 +17909,9 @@ var init_threadCodexWorkerIpc = __esm({
16419
17909
 
16420
17910
  // src/signupTaskSuggestions.ts
16421
17911
  import { spawn as spawn6 } from "node:child_process";
16422
- 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";
16423
17913
  import { tmpdir as tmpdir2 } from "node:os";
16424
- import { join as join13 } from "node:path";
17914
+ import { join as join16 } from "node:path";
16425
17915
  async function suggestSignupTasksWithCodex(args) {
16426
17916
  const attempts = 2;
16427
17917
  let previousResponse;
@@ -16443,11 +17933,11 @@ async function suggestSignupTasksWithCodex(args) {
16443
17933
  );
16444
17934
  }
16445
17935
  async function runCodexTaskSuggestion(args) {
16446
- const tempRoot = mkdtempSync3(join13(tmpdir2(), "linzumi-signup-codex-tasks-"));
16447
- const schemaPath = join13(tempRoot, "task-suggestions.schema.json");
16448
- 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");
16449
17939
  const prompt = taskSuggestionPrompt(args.previousResponse);
16450
- writeFileSync7(
17940
+ writeFileSync8(
16451
17941
  schemaPath,
16452
17942
  `${JSON.stringify(taskSuggestionJsonSchema(), null, 2)}
16453
17943
  `,
@@ -16464,7 +17954,7 @@ async function runCodexTaskSuggestion(args) {
16464
17954
  prompt
16465
17955
  })
16466
17956
  );
16467
- return readFileSync10(outputPath, "utf8");
17957
+ return readFileSync13(outputPath, "utf8");
16468
17958
  } finally {
16469
17959
  rmSync3(tempRoot, { recursive: true, force: true });
16470
17960
  }
@@ -16513,6 +18003,14 @@ function taskSuggestionPrompt(previousResponse) {
16513
18003
  "Task:",
16514
18004
  "Inspect this repository read-only and suggest exactly 5 useful quick starter tasks for Codex agents.",
16515
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
+ "",
16516
18014
  "Constraints:",
16517
18015
  "- Prefer tasks that are small, concrete, and likely to produce useful first results.",
16518
18016
  "- Do not modify files.",
@@ -16563,7 +18061,9 @@ function parseTaskSuggestionResponse(response) {
16563
18061
  const titles = tasks.map(
16564
18062
  (task) => typeof task === "object" && task !== null && !Array.isArray(task) ? task.title : void 0
16565
18063
  );
16566
- 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
+ )) {
16567
18067
  return void 0;
16568
18068
  }
16569
18069
  return titles.map((title, index) => ({
@@ -16626,8 +18126,8 @@ var init_signupTaskSuggestions = __esm({
16626
18126
 
16627
18127
  // src/remoteCodexSandboxRunner.ts
16628
18128
  import { spawn as spawn7 } from "node:child_process";
16629
- import { existsSync as existsSync9, realpathSync as realpathSync5 } from "node:fs";
16630
- 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";
16631
18131
  function createConfiguredRemoteCodexSandboxRunner(args) {
16632
18132
  const kind = normalizedSandboxKind(args.env.LINZUMI_REMOTE_CODEX_SANDBOX);
16633
18133
  if (kind === void 0) {
@@ -16646,7 +18146,7 @@ function createConfiguredRemoteCodexSandboxRunner(args) {
16646
18146
  }
16647
18147
  break;
16648
18148
  }
16649
- if (!existsSync9(sandboxBin)) {
18149
+ if (!existsSync11(sandboxBin)) {
16650
18150
  throw new Error(`remote Codex sandbox binary not found: ${sandboxBin}`);
16651
18151
  }
16652
18152
  return createRemoteCodexSandboxRunner({
@@ -16657,7 +18157,7 @@ function createConfiguredRemoteCodexSandboxRunner(args) {
16657
18157
  function createRemoteCodexSandboxRunner(config, deps = {}) {
16658
18158
  const resolvedDeps = {
16659
18159
  platform: deps.platform ?? process.platform,
16660
- exists: deps.exists ?? existsSync9,
18160
+ exists: deps.exists ?? existsSync11,
16661
18161
  spawnProcess: deps.spawnProcess ?? spawn7
16662
18162
  };
16663
18163
  return (request) => runSandboxInvocation(
@@ -16740,10 +18240,10 @@ function bubblewrapArgs(sandboxBin, request, exists) {
16740
18240
  key,
16741
18241
  value
16742
18242
  ]);
16743
- const readOnlyBindArgs = uniqueStrings2([
18243
+ const readOnlyBindArgs = uniqueStrings3([
16744
18244
  ...linuxReadOnlyRoots,
16745
- ...isAbsolute3(request.command) ? [dirname9(request.command)] : [],
16746
- dirname9(sandboxBin)
18245
+ ...isAbsolute3(request.command) ? [dirname11(request.command)] : [],
18246
+ dirname11(sandboxBin)
16747
18247
  ]).flatMap((path2) => exists(path2) ? ["--ro-bind", path2, path2] : []);
16748
18248
  const cwdParentDirs = parentDirs(request.cwd).flatMap((path2) => [
16749
18249
  "--dir",
@@ -16774,9 +18274,9 @@ function bubblewrapArgs(sandboxBin, request, exists) {
16774
18274
  ];
16775
18275
  }
16776
18276
  function macosSeatbeltProfile(request, exists) {
16777
- const readableRoots = uniqueStrings2([
18277
+ const readableRoots = uniqueStrings3([
16778
18278
  ...macosReadOnlyRoots,
16779
- ...isAbsolute3(request.command) ? [dirname9(request.command)] : []
18279
+ ...isAbsolute3(request.command) ? [dirname11(request.command)] : []
16780
18280
  ]).filter(exists);
16781
18281
  const readableRules = readableRoots.map((path2) => `(allow file-read* (subpath ${sandboxString(path2)}))`).join("\n");
16782
18282
  const writableTempRules = macosWritableTempRoots.filter(exists).flatMap((path2) => [
@@ -16858,11 +18358,11 @@ function parentDirs(path2) {
16858
18358
  (_part, index) => `/${parents.slice(0, index + 1).join("/")}`
16859
18359
  );
16860
18360
  }
16861
- function uniqueStrings2(values) {
18361
+ function uniqueStrings3(values) {
16862
18362
  return Array.from(new Set(values));
16863
18363
  }
16864
18364
  function existingPathAliases(path2) {
16865
- return uniqueStrings2([path2, realpathSync5(path2)]);
18365
+ return uniqueStrings3([path2, realpathSync5(path2)]);
16866
18366
  }
16867
18367
  function sandboxString(value) {
16868
18368
  return JSON.stringify(value);
@@ -16898,21 +18398,29 @@ var init_remoteCodexSandboxRunner = __esm({
16898
18398
  });
16899
18399
 
16900
18400
  // src/runner.ts
16901
- import { spawn as spawn8, spawnSync as spawnSync4 } from "node:child_process";
16902
- 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";
16903
18403
  import {
16904
18404
  chmodSync as chmodSync2,
16905
18405
  lstatSync,
16906
- mkdirSync as mkdirSync9,
18406
+ mkdirSync as mkdirSync10,
16907
18407
  mkdtempSync as mkdtempSync4,
16908
- readdirSync as readdirSync2,
18408
+ readdirSync as readdirSync4,
16909
18409
  realpathSync as realpathSync6,
16910
18410
  rmSync as rmSync4,
16911
- statSync
18411
+ statSync as statSync3
16912
18412
  } from "node:fs";
18413
+ import { readFile as readFile2 } from "node:fs/promises";
16913
18414
  import { createServer as createServer3 } from "node:http";
16914
18415
  import { hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
16915
- 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";
16916
18424
  async function runLocalCodexRunner(options) {
16917
18425
  const log = makeRunnerLogger(options);
16918
18426
  const cleanup = {
@@ -17172,7 +18680,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17172
18680
  });
17173
18681
  }
17174
18682
  }
17175
- const instanceId = `codex-${randomUUID3()}`;
18683
+ const instanceId = `codex-${randomUUID4()}`;
17176
18684
  const publishLocalEditorStatus = (payload) => {
17177
18685
  void kandan.push(topic, "local_editor_status", payload).catch((error) => {
17178
18686
  log("kandan.local_editor_status_push_failed", {
@@ -17247,7 +18755,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17247
18755
  };
17248
18756
  const approveEditorPortForwardCandidate = (candidate) => {
17249
18757
  const request = pendingRequestFromCandidate({
17250
- requestId: `editor-port-forward-auto-${randomUUID3()}`,
18758
+ requestId: `editor-port-forward-auto-${randomUUID4()}`,
17251
18759
  sourceSeq: 0,
17252
18760
  candidate
17253
18761
  });
@@ -17475,7 +18983,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17475
18983
  return;
17476
18984
  }
17477
18985
  const request = pendingRequestFromCandidate({
17478
- requestId: `claude-port-forward-auto-${randomUUID3()}`,
18986
+ requestId: `claude-port-forward-auto-${randomUUID4()}`,
17479
18987
  sourceSeq,
17480
18988
  candidate
17481
18989
  });
@@ -17888,6 +19396,21 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
17888
19396
  codexUrl: codexUrl ?? null,
17889
19397
  replacedRunners
17890
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
+ );
17891
19414
  const channelSession = options.channelSession === void 0 ? void 0 : await attachChannelSession({
17892
19415
  kandan,
17893
19416
  codex,
@@ -18517,6 +20040,17 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
18517
20040
  void pushHeartbeat();
18518
20041
  return;
18519
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
+ }
18520
20054
  void resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
18521
20055
  if (handled !== void 0) {
18522
20056
  return handled;
@@ -18553,9 +20087,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
18553
20087
  });
18554
20088
  });
18555
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) {
18556
20100
  controlDispatcher.value = handleControl;
18557
20101
  pendingControls.splice(0).forEach(handleControl);
18558
- return { instanceId, codexUrl, close };
20102
+ startOnboardingDiscoveryAgents(options, log);
18559
20103
  }
18560
20104
  function controlTargetsInstance(control, instanceId) {
18561
20105
  switch (control.type) {
@@ -19697,22 +21241,6 @@ ${args.developerPrompt}
19697
21241
  `;
19698
21242
  const linzumiContext = args.linzumiContext === void 0 ? "" : `
19699
21243
  ${formatLinzumiConversationContextForPrompt(args.linzumiContext)}
19700
- `;
19701
- const reviewGuidance = `
19702
- <linzumi_pr_review_guidance>
19703
- For frontend or other user-visible changes, ask whether the user wants
19704
- screenshot or screen-recording proof, ideally before/after when that helps
19705
- review the change. Screen recordings should be WebM or MP4. When the user asks
19706
- you to open or update a PR, attach or link that proof in the PR when feasible,
19707
- and state the exact blocker when it is not feasible.
19708
- </linzumi_pr_review_guidance>
19709
-
19710
- <linzumi_user_visible_writing_guide>
19711
- When you write user-visible UI, status, error, or PR-review copy, keep it short,
19712
- direct, and focused on the user's outcome. Avoid implementation-heavy sentences,
19713
- stacked clauses, and internal mechanics unless the user explicitly needs that
19714
- detail.
19715
- </linzumi_user_visible_writing_guide>
19716
21244
  `;
19717
21245
  return `<context>
19718
21246
  You are a Linzumi ${agentLabel} session launched by the Linzumi Commander.
@@ -19731,6 +21259,15 @@ Linzumi.
19731
21259
  Linzumi ${agentLabel} session: You, the inner ${agentLabel} process that performs the actual
19732
21260
  work in the approved project folder.
19733
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.
19734
21271
  </term_definitions>
19735
21272
 
19736
21273
  <linzumi_mcp>
@@ -19740,24 +21277,53 @@ read a message by ID/URL, read a thread by ID, read the scoped channel, or send
19740
21277
  a concise DM to the Commander owner when the task genuinely requires it.
19741
21278
  </linzumi_mcp>
19742
21279
 
19743
- <coding_job_metadata>
19744
- When you are working inside a coding-job thread, keep the job metadata current through the Linzumi MCP tools.
19745
- At the beginning of a job, call linzumi_upsert_coding_job_plan with your current understanding of the goal, then call linzumi_replace_coding_job_plan_steps with an ordered list of steps to accomplish that goal.
19746
- The plan is allowed to change. Rework it when the old plan is no longer a useful description of the work, but do not churn it for minor tactical adjustments.
19747
- As you make progress, call linzumi_update_coding_job_plan_step to mark the active step and completed steps. Read the metadata first when you need current step ids or lock versions.
19748
- When you open, discover, or choose the primary GitHub pull request for the job, call linzumi_link_coding_job_pull_request with the PR URL, repo owner/name, PR number, title, head/base branches, head/base SHAs, local worktree path, remote URL, and current dirty status. This link is what lets GitHub PR comments, files, commits, checks, deployments, and reviews appear automatically in the metadata overview.
19749
- The plan and goal are user-visible, so keep titles short, descriptions concrete, and completion notes focused on what was verified.
19750
- </coding_job_metadata>
19751
-
19752
21280
  <task_instructions>
19753
21281
  Work only in the approved project folder unless the human explicitly asks for
19754
21282
  something else in the Linzumi thread. Start, inspect, and modify the local app
19755
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.
19756
21303
  </task_instructions>
19757
21304
 
19758
21305
  <rules>
19759
21306
  You MUST treat the Linzumi thread as the source of truth for user-facing
19760
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.
19761
21327
  You MUST keep user-visible preview servers bound to 0.0.0.0, not 127.0.0.1 or
19762
21328
  localhost, so the Linzumi secure tunnel can reach them.
19763
21329
  You MUST keep any preview or dev server as your descendant process so the
@@ -19774,16 +21340,51 @@ revert another session's work.
19774
21340
 
19775
21341
  <examples>
19776
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
+
19777
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.
19778
21372
  </examples>
19779
21373
  ${linzumiContext}
19780
- ${reviewGuidance}
19781
21374
  ${customPrompt}
19782
21375
  <task_reminder>
19783
21376
  You are the Commander-launched Linzumi ${agentLabel} session. Do the implementation
19784
21377
  work in the approved project folder, keep preview servers reachable through the
19785
- secure tunnel, and keep the Linzumi thread truthful.
19786
- </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>`;
19787
21388
  }
19788
21389
  function availableRunnerAgentProviders(options) {
19789
21390
  switch (options.claudeCodeRunner !== void 0 || options.claudeCodeAvailable === true) {
@@ -21580,6 +23181,869 @@ function redactedThreadRunnerCliArgs(args) {
21580
23181
  function optionalCliValue(flag, value) {
21581
23182
  return value === void 0 || value === "" ? [] : [flag, value];
21582
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
+ }
21583
24047
  async function startOwnedCodexAppServer(options, args = { linzumiMcp: true }) {
21584
24048
  ensureCodexProjectTrusted(options.cwd);
21585
24049
  const defaults = runnerRuntimeDefaults(options);
@@ -21636,9 +24100,9 @@ function mcpOwnerUsername(options) {
21636
24100
  return options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
21637
24101
  }
21638
24102
  function writeEphemeralMcpAuthFile(options) {
21639
- const directory = mkdtempSync4(join14(tmpdir3(), "linzumi-mcp-auth-"));
24103
+ const directory = mkdtempSync4(join17(tmpdir3(), "linzumi-mcp-auth-"));
21640
24104
  chmodSync2(directory, 448);
21641
- const path2 = join14(directory, "auth.json");
24105
+ const path2 = join17(directory, "auth.json");
21642
24106
  writeCachedLocalRunnerToken({
21643
24107
  kandanUrl: options.kandanUrl,
21644
24108
  accessToken: options.token,
@@ -21714,7 +24178,7 @@ function configuredAllowedCwds(values, options = {}) {
21714
24178
  const absolutePath = resolve7(expandUserPath(value));
21715
24179
  try {
21716
24180
  if (options.createMissing === true) {
21717
- mkdirSync9(absolutePath, { recursive: true });
24181
+ mkdirSync10(absolutePath, { recursive: true });
21718
24182
  }
21719
24183
  const realPath = realpathSync6(absolutePath);
21720
24184
  allowedCwds.push(
@@ -21751,9 +24215,9 @@ function allowedCwdProjects(allowedCwds) {
21751
24215
  });
21752
24216
  }
21753
24217
  function isGitProjectDirectory(cwd) {
21754
- const gitPath = join14(cwd, ".git");
24218
+ const gitPath = join17(cwd, ".git");
21755
24219
  try {
21756
- const gitPathStats = statSync(gitPath);
24220
+ const gitPathStats = statSync3(gitPath);
21757
24221
  return gitPathStats.isDirectory() || gitPathStats.isFile();
21758
24222
  } catch {
21759
24223
  return false;
@@ -21764,7 +24228,7 @@ function browseRunnerDirectory(control, options) {
21764
24228
  const requestedPath = stringValue(control.path) ?? currentHomeDirectory();
21765
24229
  try {
21766
24230
  const currentPath = realpathSync6(resolve7(expandUserPath(requestedPath)));
21767
- const stats = statSync(currentPath);
24231
+ const stats = statSync3(currentPath);
21768
24232
  if (!stats.isDirectory()) {
21769
24233
  return {
21770
24234
  instanceId: options.runnerId,
@@ -21774,9 +24238,9 @@ function browseRunnerDirectory(control, options) {
21774
24238
  error: "not_directory"
21775
24239
  };
21776
24240
  }
21777
- const parent = dirname10(currentPath);
21778
- const entries = readdirSync2(currentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => visibleRunnerDirectoryEntryName(entry.name)).map((entry) => {
21779
- 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);
21780
24244
  return {
21781
24245
  name: entry.name,
21782
24246
  path: path2,
@@ -21817,7 +24281,7 @@ function projectDirectoryName(name) {
21817
24281
  function availableProjectDirectoryName(projectsRoot, baseName, suffix = 0) {
21818
24282
  for (let nextSuffix = suffix; ; nextSuffix += 1) {
21819
24283
  const candidate = nextSuffix === 0 ? baseName : `${baseName}-${nextSuffix}`;
21820
- if (!projectPathExists(join14(projectsRoot, candidate))) {
24284
+ if (!projectPathExists(join17(projectsRoot, candidate))) {
21821
24285
  return candidate;
21822
24286
  }
21823
24287
  }
@@ -21845,9 +24309,9 @@ function createRunnerProject(control, options, allowedCwds) {
21845
24309
  error: "invalid_project_template"
21846
24310
  };
21847
24311
  }
21848
- const projectsRoot = join14(currentHomeDirectory(), "linzumi");
24312
+ const projectsRoot = join17(currentHomeDirectory(), "linzumi");
21849
24313
  const resolvedProjectDirName = template === "hello_linzumi_demo" ? availableProjectDirectoryName(projectsRoot, projectDirName) : projectDirName;
21850
- const projectPath = join14(projectsRoot, resolvedProjectDirName);
24314
+ const projectPath = join17(projectsRoot, resolvedProjectDirName);
21851
24315
  let createdProjectPath = false;
21852
24316
  try {
21853
24317
  if (template !== "hello_linzumi_demo" && projectPathExists(projectPath)) {
@@ -21859,7 +24323,7 @@ function createRunnerProject(control, options, allowedCwds) {
21859
24323
  error: "project_directory_exists"
21860
24324
  };
21861
24325
  }
21862
- mkdirSync9(projectsRoot, { recursive: true });
24326
+ mkdirSync10(projectsRoot, { recursive: true });
21863
24327
  if (template === "hello_linzumi_demo") {
21864
24328
  createdProjectPath = true;
21865
24329
  createHelloLinzumiProject({
@@ -21867,10 +24331,10 @@ function createRunnerProject(control, options, allowedCwds) {
21867
24331
  name: resolvedProjectDirName
21868
24332
  });
21869
24333
  } else {
21870
- mkdirSync9(projectPath, { recursive: false });
24334
+ mkdirSync10(projectPath, { recursive: false });
21871
24335
  createdProjectPath = true;
21872
24336
  }
21873
- const git = spawnSync4("git", ["init"], {
24337
+ const git = spawnSync5("git", ["init"], {
21874
24338
  cwd: projectPath,
21875
24339
  encoding: "utf8",
21876
24340
  env: process.env
@@ -21978,7 +24442,7 @@ async function suggestRunnerTasks(control, options, allowedCwds) {
21978
24442
  };
21979
24443
  }
21980
24444
  }
21981
- 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;
21982
24446
  var init_runner = __esm({
21983
24447
  "src/runner.ts"() {
21984
24448
  "use strict";
@@ -22009,13 +24473,20 @@ var init_runner = __esm({
22009
24473
  init_runnerConsoleReporter();
22010
24474
  init_streamDeltaQueue();
22011
24475
  init_version();
24476
+ init_telemetry();
22012
24477
  init_mcpConfig();
24478
+ init_linzumiApiClient();
24479
+ init_onboardingConversationDiscovery();
24480
+ init_onboardingProjectDiscovery();
22013
24481
  init_authCache();
22014
24482
  init_threadCodexWorkerIpc();
22015
24483
  init_signupTaskSuggestions();
22016
24484
  init_userFacingErrors();
22017
24485
  init_remoteCodexSandboxRunner();
22018
24486
  THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
24487
+ onboardingDiscoveryAgentStartKeys = /* @__PURE__ */ new Set();
24488
+ onboardingConversationImportKeys = /* @__PURE__ */ new Set();
24489
+ onboardingConversationDiscoveryReportLimit = 100;
22019
24490
  claudeSessionStoreSequenceHighWater = /* @__PURE__ */ new Map();
22020
24491
  ClaudeCodeOutputPostError = class extends Error {
22021
24492
  constructor(cause) {
@@ -22024,11 +24495,17 @@ var init_runner = __esm({
22024
24495
  this.name = "ClaudeCodeOutputPostError";
22025
24496
  }
22026
24497
  };
24498
+ onboardingConversationTitleFirstMessages = 4;
24499
+ onboardingConversationTitleLastMessages = 4;
24500
+ onboardingConversationTitleBodyMaxLength = 1200;
24501
+ onboardingConversationImportMessageMaxLength = 15500;
24502
+ onboardingConversationImportCandidateLimit = 500;
24503
+ onboardingConversationImportProgressInterval = 25;
22027
24504
  }
22028
24505
  });
22029
24506
 
22030
24507
  // src/kandanTls.ts
22031
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
24508
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "node:fs";
22032
24509
  import { Agent } from "undici";
22033
24510
  import WsWebSocket from "ws";
22034
24511
  function kandanTlsTrustFromEnv() {
@@ -22039,10 +24516,10 @@ function kandanTlsTrustFromCaFile(caFile) {
22039
24516
  return void 0;
22040
24517
  }
22041
24518
  const trimmed = caFile.trim();
22042
- if (!existsSync10(trimmed)) {
24519
+ if (!existsSync12(trimmed)) {
22043
24520
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
22044
24521
  }
22045
- const ca = readFileSync11(trimmed, "utf8");
24522
+ const ca = readFileSync14(trimmed, "utf8");
22046
24523
  return {
22047
24524
  caFile: trimmed,
22048
24525
  ca,
@@ -40825,11 +43302,11 @@ var init_RemoveFileError = __esm({
40825
43302
  });
40826
43303
 
40827
43304
  // ../../node_modules/@inquirer/external-editor/dist/esm/index.js
40828
- import { spawn as spawn10, spawnSync as spawnSync5 } from "child_process";
40829
- 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";
40830
43307
  import path from "node:path";
40831
43308
  import os from "node:os";
40832
- import { randomUUID as randomUUID4 } from "node:crypto";
43309
+ import { randomUUID as randomUUID5 } from "node:crypto";
40833
43310
  function editAsync(text2 = "", callback, fileOptions) {
40834
43311
  const editor = new ExternalEditor(text2, fileOptions);
40835
43312
  editor.runAsync((err, result) => {
@@ -40929,7 +43406,7 @@ var init_esm5 = __esm({
40929
43406
  createTemporaryFile() {
40930
43407
  try {
40931
43408
  const baseDir = this.fileOptions.dir ?? os.tmpdir();
40932
- const id = randomUUID4();
43409
+ const id = randomUUID5();
40933
43410
  const prefix = sanitizeAffix(this.fileOptions.prefix);
40934
43411
  const postfix = sanitizeAffix(this.fileOptions.postfix);
40935
43412
  const filename = `${prefix}${id}${postfix}`;
@@ -40943,14 +43420,14 @@ var init_esm5 = __esm({
40943
43420
  if (Object.prototype.hasOwnProperty.call(this.fileOptions, "mode")) {
40944
43421
  opt.mode = this.fileOptions.mode;
40945
43422
  }
40946
- writeFileSync10(this.tempFile, this.text, opt);
43423
+ writeFileSync11(this.tempFile, this.text, opt);
40947
43424
  } catch (createFileError) {
40948
43425
  throw new CreateFileError(createFileError);
40949
43426
  }
40950
43427
  }
40951
43428
  readTemporaryFile() {
40952
43429
  try {
40953
- const tempFileBuffer = readFileSync14(this.tempFile);
43430
+ const tempFileBuffer = readFileSync17(this.tempFile);
40954
43431
  if (tempFileBuffer.length === 0) {
40955
43432
  this.text = "";
40956
43433
  } else {
@@ -40973,7 +43450,7 @@ var init_esm5 = __esm({
40973
43450
  }
40974
43451
  launchEditor() {
40975
43452
  try {
40976
- 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" });
40977
43454
  this.lastExitStatus = editorProcess.status ?? 0;
40978
43455
  } catch (launchError) {
40979
43456
  throw new LaunchEditorError(launchError);
@@ -42315,21 +44792,21 @@ __export(signupFlow_exports, {
42315
44792
  signupTaskSuggestionWaitMessageForTest: () => signupTaskSuggestionWaitMessageForTest,
42316
44793
  toggleProjectPickerSelectionForTest: () => toggleProjectPickerSelectionForTest
42317
44794
  });
42318
- import { spawn as spawn11, spawnSync as spawnSync6 } from "node:child_process";
44795
+ import { spawn as spawn11, spawnSync as spawnSync7 } from "node:child_process";
42319
44796
  import {
42320
- existsSync as existsSync13,
44797
+ existsSync as existsSync15,
42321
44798
  constants as fsConstants,
42322
- mkdirSync as mkdirSync12,
44799
+ mkdirSync as mkdirSync13,
42323
44800
  mkdtempSync as mkdtempSync5,
42324
- readFileSync as readFileSync15,
42325
- readdirSync as readdirSync3,
44801
+ readFileSync as readFileSync18,
44802
+ readdirSync as readdirSync5,
42326
44803
  rmSync as rmSync5,
42327
- statSync as statSync2,
42328
- writeFileSync as writeFileSync11
44804
+ statSync as statSync4,
44805
+ writeFileSync as writeFileSync12
42329
44806
  } from "node:fs";
42330
44807
  import { access } from "node:fs/promises";
42331
- import { homedir as homedir12, tmpdir as tmpdir4 } from "node:os";
42332
- 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";
42333
44810
  import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
42334
44811
  import { emitKeypressEvents } from "node:readline";
42335
44812
  function signupHelpText() {
@@ -42445,12 +44922,12 @@ function defaultSignupDraftStore(serviceUrl) {
42445
44922
  const path2 = defaultSignupDraftPath(serviceUrl);
42446
44923
  return {
42447
44924
  read: () => {
42448
- if (!existsSync13(path2)) {
44925
+ if (!existsSync15(path2)) {
42449
44926
  return void 0;
42450
44927
  }
42451
44928
  let parsed;
42452
44929
  try {
42453
- parsed = JSON.parse(readFileSync15(path2, "utf8"));
44930
+ parsed = JSON.parse(readFileSync18(path2, "utf8"));
42454
44931
  } catch (_error) {
42455
44932
  return void 0;
42456
44933
  }
@@ -42460,8 +44937,8 @@ function defaultSignupDraftStore(serviceUrl) {
42460
44937
  return comparableServiceUrl(parsed.serviceUrl) === comparableServiceUrl(serviceUrl) ? parsed : void 0;
42461
44938
  },
42462
44939
  write: (draft) => {
42463
- mkdirSync12(dirname13(path2), { recursive: true });
42464
- writeFileSync11(path2, `${JSON.stringify(draft, null, 2)}
44940
+ mkdirSync13(dirname15(path2), { recursive: true });
44941
+ writeFileSync12(path2, `${JSON.stringify(draft, null, 2)}
42465
44942
  `, {
42466
44943
  encoding: "utf8",
42467
44944
  mode: 384
@@ -42473,8 +44950,8 @@ function defaultSignupDraftStore(serviceUrl) {
42473
44950
  };
42474
44951
  }
42475
44952
  function defaultSignupDraftPath(serviceUrl) {
42476
- return join17(
42477
- dirname13(localConfigPath()),
44953
+ return join20(
44954
+ dirname15(localConfigPath()),
42478
44955
  `signup-draft.${localConfigScopeFileStem(serviceUrl)}.json`
42479
44956
  );
42480
44957
  }
@@ -42508,8 +44985,8 @@ function defaultSignupTaskCacheStore(serviceUrl) {
42508
44985
  }
42509
44986
  }
42510
44987
  };
42511
- mkdirSync12(dirname13(path2), { recursive: true });
42512
- writeFileSync11(path2, `${JSON.stringify(next, null, 2)}
44988
+ mkdirSync13(dirname15(path2), { recursive: true });
44989
+ writeFileSync12(path2, `${JSON.stringify(next, null, 2)}
42513
44990
  `, {
42514
44991
  encoding: "utf8",
42515
44992
  mode: 384
@@ -42518,18 +44995,18 @@ function defaultSignupTaskCacheStore(serviceUrl) {
42518
44995
  };
42519
44996
  }
42520
44997
  function defaultSignupTaskCachePath(serviceUrl) {
42521
- return join17(
42522
- dirname13(localConfigPath()),
44998
+ return join20(
44999
+ dirname15(localConfigPath()),
42523
45000
  `signup-task-cache.${localConfigScopeFileStem(serviceUrl)}.json`
42524
45001
  );
42525
45002
  }
42526
45003
  function readSignupTaskCache(path2) {
42527
- if (!existsSync13(path2)) {
45004
+ if (!existsSync15(path2)) {
42528
45005
  return { version: 1, entries: {} };
42529
45006
  }
42530
45007
  let parsed;
42531
45008
  try {
42532
- parsed = JSON.parse(readFileSync15(path2, "utf8"));
45009
+ parsed = JSON.parse(readFileSync18(path2, "utf8"));
42533
45010
  } catch (_error) {
42534
45011
  return { version: 1, entries: {} };
42535
45012
  }
@@ -43715,6 +46192,7 @@ async function signupCommanderRunnerOptions(args, runnerId) {
43715
46192
  editorRuntime: editorRuntime.runtime,
43716
46193
  socketFactory: trustedWebSocketFactory(trust),
43717
46194
  dependencyStatus,
46195
+ onboardingDiscovery: "start",
43718
46196
  runtimeDefaults: {
43719
46197
  model: void 0,
43720
46198
  reasoningEffort: void 0,
@@ -44413,7 +46891,7 @@ function autocompletePathSuggestions(pathInput) {
44413
46891
  try {
44414
46892
  const showHidden = prefix.startsWith(".");
44415
46893
  const currentPath = directoryExists(normalizedInput) ? [normalizedInput.replace(/\/$/, "") || "/"] : [];
44416
- 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) => ({
44417
46895
  path: path2,
44418
46896
  score: fuzzyPathSegmentScore(path2.split("/").at(-1) ?? path2, prefix)
44419
46897
  })).filter((candidate) => candidate.score !== void 0).sort((left, right) => {
@@ -44659,7 +47137,7 @@ async function runSignupPreflights(runtime) {
44659
47137
  function defaultPreflightRuntime() {
44660
47138
  return {
44661
47139
  cwd: process.cwd(),
44662
- homeDir: homedir12(),
47140
+ homeDir: homedir14(),
44663
47141
  probeTool,
44664
47142
  readGitEmail,
44665
47143
  discoverCodeRoots,
@@ -44804,13 +47282,13 @@ async function suggestTasksWithCodex(projectPath, retryPolicy) {
44804
47282
  );
44805
47283
  }
44806
47284
  async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolicy) {
44807
- const tempRoot = mkdtempSync5(join17(tmpdir4(), "linzumi-signup-codex-tasks-"));
44808
- const schemaPath = join17(tempRoot, "task-suggestions.schema.json");
44809
- 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");
44810
47288
  const model = process.env.LINZUMI_SIGNUP_TASK_MODEL ?? "gpt-5.4-mini";
44811
47289
  const prompt = taskSuggestionPrompt2(previousResponse);
44812
47290
  const codexCommand = await resolveSignupCodexCommand();
44813
- writeFileSync11(
47291
+ writeFileSync12(
44814
47292
  schemaPath,
44815
47293
  `${JSON.stringify(taskSuggestionJsonSchema2(), null, 2)}
44816
47294
  `,
@@ -44831,7 +47309,7 @@ async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolic
44831
47309
  ),
44832
47310
  retryPolicy
44833
47311
  );
44834
- return readFileSync15(outputPath, "utf8");
47312
+ return readFileSync18(outputPath, "utf8");
44835
47313
  } finally {
44836
47314
  rmSync5(tempRoot, { recursive: true, force: true });
44837
47315
  }
@@ -44868,7 +47346,7 @@ function codexTaskSuggestionProcess2(args) {
44868
47346
  function signupCodexTaskSuggestionProcessForTest(args) {
44869
47347
  return codexTaskSuggestionProcess2(args);
44870
47348
  }
44871
- async function resolveSignupCodexCommand(env = process.env, homeDir = homedir12(), executableExists = fileIsExecutable) {
47349
+ async function resolveSignupCodexCommand(env = process.env, homeDir = homedir14(), executableExists = fileIsExecutable) {
44872
47350
  const override = firstConfiguredValue([
44873
47351
  env.LINZUMI_SIGNUP_CODEX_BIN,
44874
47352
  env.LINZUMI_CODEX_BIN
@@ -44923,7 +47401,7 @@ function resolveHomePath(path2, homeDir) {
44923
47401
  return homeDir;
44924
47402
  }
44925
47403
  if (path2.startsWith("~/")) {
44926
- return join17(homeDir, path2.slice(2));
47404
+ return join20(homeDir, path2.slice(2));
44927
47405
  }
44928
47406
  return resolve9(path2);
44929
47407
  }
@@ -44936,9 +47414,9 @@ function commandLooksPathLike(command) {
44936
47414
  }
44937
47415
  function homeManagedCodexCandidates(homeDir) {
44938
47416
  return [
44939
- join17(homeDir, ".volta", "bin", "codex"),
44940
- join17(homeDir, ".local", "bin", "codex"),
44941
- join17(homeDir, "bin", "codex")
47417
+ join20(homeDir, ".volta", "bin", "codex"),
47418
+ join20(homeDir, ".local", "bin", "codex"),
47419
+ join20(homeDir, "bin", "codex")
44942
47420
  ];
44943
47421
  }
44944
47422
  async function firstExecutablePath(paths, executableExists) {
@@ -44953,7 +47431,7 @@ function commandPathCandidates(path2) {
44953
47431
  if (path2 === void 0 || path2.trim() === "") {
44954
47432
  return [];
44955
47433
  }
44956
- 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"));
44957
47435
  }
44958
47436
  async function fileIsExecutable(path2) {
44959
47437
  try {
@@ -44978,6 +47456,14 @@ function taskSuggestionPrompt2(previousResponse) {
44978
47456
  "Task:",
44979
47457
  "Inspect this repository read-only and suggest exactly 5 useful quick starter tasks for Codex agents.",
44980
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
+ "",
44981
47467
  "Constraints:",
44982
47468
  "- Prefer tasks that are small, concrete, and likely to produce useful first results.",
44983
47469
  "- Do not modify files.",
@@ -45178,11 +47664,11 @@ function codexPreflightLocation(command, homeDir) {
45178
47664
  switch (command) {
45179
47665
  case "codex":
45180
47666
  return "on PATH";
45181
- case join17(homeDir, ".volta", "bin", "codex"):
47667
+ case join20(homeDir, ".volta", "bin", "codex"):
45182
47668
  return "via Volta";
45183
- case join17(homeDir, ".local", "bin", "codex"):
47669
+ case join20(homeDir, ".local", "bin", "codex"):
45184
47670
  return "via ~/.local/bin";
45185
- case join17(homeDir, "bin", "codex"):
47671
+ case join20(homeDir, "bin", "codex"):
45186
47672
  return "via ~/bin";
45187
47673
  default:
45188
47674
  return "from configured path";
@@ -45287,7 +47773,7 @@ function spawnSyncGit(args, cwd) {
45287
47773
  }
45288
47774
  function spawnSyncGitOutput(args, cwd) {
45289
47775
  try {
45290
- const result = spawnSync6("git", [...args], {
47776
+ const result = spawnSync7("git", [...args], {
45291
47777
  cwd,
45292
47778
  stdio: ["ignore", "pipe", "ignore"]
45293
47779
  });
@@ -45337,13 +47823,13 @@ function probeToolWithArgs(command, args, cwd) {
45337
47823
  }
45338
47824
  async function discoverCodeRoots(homeDir) {
45339
47825
  const candidates = ["src", "code", "projects"].map(
45340
- (name) => join17(homeDir, name)
47826
+ (name) => join20(homeDir, name)
45341
47827
  );
45342
- return candidates.filter((path2) => existsSync13(path2)).flatMap((path2) => discoveredProjectNames(path2));
47828
+ return candidates.filter((path2) => existsSync15(path2)).flatMap((path2) => discoveredProjectNames(path2));
45343
47829
  }
45344
47830
  function discoveredProjectNames(root) {
45345
47831
  try {
45346
- 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}`);
45347
47833
  } catch {
45348
47834
  return [];
45349
47835
  }
@@ -45391,7 +47877,7 @@ function discoverProjectsFromCurrentDirectory(cwd) {
45391
47877
  }
45392
47878
  function discoverProjectsFromGuessedRoots(homeDir) {
45393
47879
  const guessedRoots = ["src", "code", "projects"].map(
45394
- (name) => join17(homeDir, name)
47880
+ (name) => join20(homeDir, name)
45395
47881
  );
45396
47882
  const projects = guessedRoots.flatMap(
45397
47883
  (root) => discoverProjectsUnderRoot(root)
@@ -45443,25 +47929,25 @@ function looksLikeProject(path2) {
45443
47929
  "pnpm-lock.yaml",
45444
47930
  "yarn.lock",
45445
47931
  "package-lock.json"
45446
- ].some((name) => existsSync13(join17(path2, name)));
47932
+ ].some((name) => existsSync15(join20(path2, name)));
45447
47933
  }
45448
47934
  function detectProjectLanguage(path2) {
45449
- if (existsSync13(join17(path2, "pyproject.toml")) || existsSync13(join17(path2, "requirements.txt"))) {
47935
+ if (existsSync15(join20(path2, "pyproject.toml")) || existsSync15(join20(path2, "requirements.txt"))) {
45450
47936
  return "Python";
45451
47937
  }
45452
- if (existsSync13(join17(path2, "Cargo.toml"))) {
47938
+ if (existsSync15(join20(path2, "Cargo.toml"))) {
45453
47939
  return "Rust";
45454
47940
  }
45455
- if (existsSync13(join17(path2, "go.mod"))) {
47941
+ if (existsSync15(join20(path2, "go.mod"))) {
45456
47942
  return "Go";
45457
47943
  }
45458
- if (existsSync13(join17(path2, "mix.exs"))) {
47944
+ if (existsSync15(join20(path2, "mix.exs"))) {
45459
47945
  return "Elixir";
45460
47946
  }
45461
- if (existsSync13(join17(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
47947
+ if (existsSync15(join20(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
45462
47948
  return "TypeScript";
45463
47949
  }
45464
- if (existsSync13(join17(path2, "package.json"))) {
47950
+ if (existsSync15(join20(path2, "package.json"))) {
45465
47951
  return "JavaScript";
45466
47952
  }
45467
47953
  return "Project";
@@ -45469,7 +47955,7 @@ function detectProjectLanguage(path2) {
45469
47955
  function packageJsonMentionsTypeScript(path2) {
45470
47956
  try {
45471
47957
  const packageJson2 = JSON.parse(
45472
- readFileSync15(join17(path2, "package.json"), "utf8")
47958
+ readFileSync18(join20(path2, "package.json"), "utf8")
45473
47959
  );
45474
47960
  return packageJson2.dependencies?.typescript !== void 0 || packageJson2.devDependencies?.typescript !== void 0;
45475
47961
  } catch {
@@ -45477,11 +47963,11 @@ function packageJsonMentionsTypeScript(path2) {
45477
47963
  }
45478
47964
  }
45479
47965
  function hasGitMetadata(path2) {
45480
- return existsSync13(join17(path2, ".git"));
47966
+ return existsSync15(join20(path2, ".git"));
45481
47967
  }
45482
47968
  function childDirectories(root) {
45483
47969
  try {
45484
- 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));
45485
47971
  } catch {
45486
47972
  return [];
45487
47973
  }
@@ -45500,17 +47986,17 @@ function ignoredProjectDirectory(path2) {
45500
47986
  }
45501
47987
  function directoryExists(path2) {
45502
47988
  try {
45503
- return statSync2(path2).isDirectory();
47989
+ return statSync4(path2).isDirectory();
45504
47990
  } catch {
45505
47991
  return false;
45506
47992
  }
45507
47993
  }
45508
47994
  function expandHomePath(path2) {
45509
47995
  if (path2 === "~") {
45510
- return homedir12();
47996
+ return homedir14();
45511
47997
  }
45512
47998
  if (path2.startsWith("~/")) {
45513
- return join17(homedir12(), path2.slice(2));
47999
+ return join20(homedir14(), path2.slice(2));
45514
48000
  }
45515
48001
  return resolve9(path2);
45516
48002
  }
@@ -45599,8 +48085,8 @@ secure mission control for all your agents on your computers
45599
48085
  init_runner();
45600
48086
  init_claudeCodeSession();
45601
48087
  init_authCache();
45602
- import { existsSync as existsSync14, readFileSync as readFileSync16, realpathSync as realpathSync7 } from "node:fs";
45603
- 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";
45604
48090
  import { resolve as resolve10 } from "node:path";
45605
48091
  import { fileURLToPath as fileURLToPath4 } from "node:url";
45606
48092
 
@@ -45621,7 +48107,8 @@ async function resolveLocalRunnerToken(args, deps = {
45621
48107
  kandanUrl: args.kandanUrl,
45622
48108
  accessToken: cached.accessToken,
45623
48109
  workspaceSlug: args.workspaceSlug,
45624
- channelSlug: args.channelSlug
48110
+ channelSlug: args.channelSlug,
48111
+ requiredScopes: args.requiredScopes
45625
48112
  });
45626
48113
  if (cachedTokenIsUsable) {
45627
48114
  return cached.accessToken;
@@ -45661,9 +48148,9 @@ init_kandanTls();
45661
48148
  init_protocol();
45662
48149
  init_json();
45663
48150
  init_defaultUrls();
45664
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
45665
- import { dirname as dirname11, join as join15 } from "node:path";
45666
- 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";
45667
48154
  async function runAgentCliCommand(args, deps = {
45668
48155
  fetchImpl: fetch,
45669
48156
  stdout: process.stdout,
@@ -46371,7 +48858,7 @@ function agentTokenFile(flags) {
46371
48858
  return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
46372
48859
  }
46373
48860
  function defaultAgentTokenFilePath() {
46374
- return join15(homedir10(), ".linzumi", "agent-token.json");
48861
+ return join18(homedir12(), ".linzumi", "agent-token.json");
46375
48862
  }
46376
48863
  function normalizedApiUrl(apiUrl) {
46377
48864
  return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
@@ -46380,11 +48867,11 @@ function authorizationHeaders(token) {
46380
48867
  return { authorization: `Bearer ${token}` };
46381
48868
  }
46382
48869
  function readOptionalTextFile(path2) {
46383
- return existsSync11(path2) ? readFileSync12(path2, "utf8") : void 0;
48870
+ return existsSync13(path2) ? readFileSync15(path2, "utf8") : void 0;
46384
48871
  }
46385
48872
  function writeTextFile(path2, content) {
46386
- mkdirSync10(dirname11(path2), { recursive: true });
46387
- writeFileSync8(path2, content);
48873
+ mkdirSync11(dirname13(path2), { recursive: true });
48874
+ writeFileSync9(path2, content);
46388
48875
  }
46389
48876
  function readStoredAgentTokenFile(path2, readTextFile = readOptionalTextFile) {
46390
48877
  const content = readTextFile(path2);
@@ -46471,27 +48958,27 @@ init_helloLinzumiProject();
46471
48958
  // src/commanderDaemon.ts
46472
48959
  init_runnerLogger();
46473
48960
  import {
46474
- existsSync as existsSync12,
46475
- closeSync as closeSync2,
46476
- mkdirSync as mkdirSync11,
46477
- openSync as openSync3,
46478
- readFileSync as readFileSync13,
48961
+ existsSync as existsSync14,
48962
+ closeSync as closeSync3,
48963
+ mkdirSync as mkdirSync12,
48964
+ openSync as openSync4,
48965
+ readFileSync as readFileSync16,
46479
48966
  watch,
46480
- writeFileSync as writeFileSync9
48967
+ writeFileSync as writeFileSync10
46481
48968
  } from "node:fs";
46482
- import { homedir as homedir11 } from "node:os";
46483
- 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";
46484
48971
  import { execFileSync, spawn as spawn9 } from "node:child_process";
46485
48972
  import { fileURLToPath as fileURLToPath3 } from "node:url";
46486
48973
  var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
46487
48974
  function commanderStatusDir() {
46488
- return join16(homedir11(), ".linzumi", "commanders");
48975
+ return join19(homedir13(), ".linzumi", "commanders");
46489
48976
  }
46490
48977
  function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
46491
- return join16(statusDir, `${safeRunnerId(runnerId)}.json`);
48978
+ return join19(statusDir, `${safeRunnerId(runnerId)}.json`);
46492
48979
  }
46493
48980
  function defaultCommanderLogFile(runnerId) {
46494
- return join16(homedir11(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
48981
+ return join19(homedir13(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
46495
48982
  }
46496
48983
  function commanderLogIsConnected(log) {
46497
48984
  return connectedMarkers.some((marker) => log.includes(marker));
@@ -46512,10 +48999,10 @@ function startCommanderDaemon(options) {
46512
48999
  "--log-file",
46513
49000
  logFile
46514
49001
  ];
46515
- mkdirSync11(statusDir, { recursive: true });
46516
- mkdirSync11(dirname12(logFile), { recursive: true });
46517
- const out = openSync3(logFile, "a");
46518
- 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");
46519
49006
  writeCliAuditEvent(
46520
49007
  "process.spawn",
46521
49008
  {
@@ -46542,8 +49029,8 @@ function startCommanderDaemon(options) {
46542
49029
  },
46543
49030
  { sessionId: options.runnerId }
46544
49031
  );
46545
- closeSync2(out);
46546
- closeSync2(err);
49032
+ closeSync3(out);
49033
+ closeSync3(err);
46547
49034
  child.unref();
46548
49035
  if (child.pid === void 0) {
46549
49036
  throw new Error("commander daemon did not report a pid");
@@ -46559,21 +49046,21 @@ function startCommanderDaemon(options) {
46559
49046
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
46560
49047
  command: [nodeBin, ...command]
46561
49048
  };
46562
- writeFileSync9(statusFile, `${JSON.stringify(record, null, 2)}
49049
+ writeFileSync10(statusFile, `${JSON.stringify(record, null, 2)}
46563
49050
  `);
46564
49051
  return record;
46565
49052
  }
46566
49053
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
46567
49054
  const statusFile = commanderStatusFile(runnerId, statusDir);
46568
- if (!existsSync12(statusFile)) {
49055
+ if (!existsSync14(statusFile)) {
46569
49056
  return { status: "missing", runnerId, statusFile };
46570
49057
  }
46571
- const record = parseRecord(readFileSync13(statusFile, "utf8"));
49058
+ const record = parseRecord(readFileSync16(statusFile, "utf8"));
46572
49059
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
46573
49060
  }
46574
49061
  async function waitForCommanderDaemon(options) {
46575
49062
  const now = options.now ?? (() => Date.now());
46576
- 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);
46577
49064
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
46578
49065
  const deadline = now() + options.timeoutMs;
46579
49066
  while (now() <= deadline) {
@@ -46772,6 +49259,7 @@ async function waitForFileChangeOrTimeout(path2, deadline, now, ready2 = () => f
46772
49259
 
46773
49260
  // src/index.ts
46774
49261
  init_version();
49262
+ init_telemetry();
46775
49263
 
46776
49264
  // ../../node_modules/zod/v3/external.js
46777
49265
  var external_exports = {};
@@ -54437,7 +56925,7 @@ var StdioServerTransport = class {
54437
56925
  };
54438
56926
 
54439
56927
  // src/mcpServer.ts
54440
- import { readFile as readFile2 } from "node:fs/promises";
56928
+ import { readFile as readFile3 } from "node:fs/promises";
54441
56929
 
54442
56930
  // node_modules/zod/v3/external.js
54443
56931
  var external_exports2 = {};
@@ -58484,78 +60972,19 @@ var NEVER2 = INVALID2;
58484
60972
  init_authCache();
58485
60973
  init_commanderAttachments();
58486
60974
  init_json();
58487
-
58488
- // src/linzumiApiClient.ts
58489
- init_oauth();
58490
- init_protocol();
58491
- function createLinzumiMcpApiClient(options) {
58492
- const fetchImpl = options.fetchImpl ?? fetch;
58493
- const baseUrl = kandanHttpBaseUrl(options.kandanUrl);
58494
- const apiPrefix = options.authMode === "personal-agent-delegation" ? "/api/v2/personal-agent-mcp" : "/api/v2/local-runner-mcp";
58495
- const operatingMode = options.operatingMode ?? "text";
58496
- const request = async (method, path2, params) => {
58497
- const url = new URL(path2, baseUrl);
58498
- const paramsWithMode = {
58499
- ...params,
58500
- operating_mode: operatingMode
58501
- };
58502
- const requestInit = {
58503
- method,
58504
- headers: { authorization: `Bearer ${options.accessToken}` }
58505
- };
58506
- if (method === "GET") {
58507
- for (const [key, value] of Object.entries(paramsWithMode)) {
58508
- if (value !== void 0 && value !== null) {
58509
- url.searchParams.set(key, String(value));
58510
- }
58511
- }
58512
- } else {
58513
- requestInit.headers = {
58514
- ...requestInit.headers,
58515
- "content-type": "application/json"
58516
- };
58517
- requestInit.body = JSON.stringify(paramsWithMode);
58518
- }
58519
- const response = await fetchImpl(url, requestInit);
58520
- const parsed = await response.json();
58521
- const body = isJsonObject(parsed) ? parsed : void 0;
58522
- if (body === void 0) {
58523
- throw new Error(`Linzumi MCP API returned non-object JSON from ${path2}`);
58524
- }
58525
- if (response.ok && body.ok === true) {
58526
- return body;
58527
- }
58528
- const error = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
58529
- throw new Error(`Linzumi MCP API ${path2} failed: ${error}`);
58530
- };
58531
- return {
58532
- validateAuth: () => request("GET", `${apiPrefix}/validate`, {}),
58533
- getMessage: (params) => request("GET", `${apiPrefix}/message`, params),
58534
- getThread: (params) => request("GET", `${apiPrefix}/thread`, params),
58535
- getChannel: (params) => request("GET", `${apiPrefix}/channel`, params),
58536
- getCodingJobMetadata: (params) => request("GET", `${apiPrefix}/coding-job-metadata`, params),
58537
- upsertCodingJobPlan: (params) => request("POST", `${apiPrefix}/coding-job-plan`, params),
58538
- replaceCodingJobPlanSteps: (params) => request("POST", `${apiPrefix}/coding-job-plan/steps`, params),
58539
- updateCodingJobPlanStep: (params) => request("POST", `${apiPrefix}/coding-job-plan/step`, params),
58540
- linkCodingJobPullRequest: (params) => request("POST", `${apiPrefix}/coding-job-primary-pr`, params),
58541
- listVaultSecrets: (params) => request("GET", `${apiPrefix}/vault-secrets`, params),
58542
- sendChannelMessage: (params) => request("POST", `${apiPrefix}/channel-message`, params),
58543
- prepareMessageUploads: (params) => request("POST", `${apiPrefix}/message-uploads/prepare`, params),
58544
- attachMessageFiles: (params) => request("POST", `${apiPrefix}/message-files/attach`, params),
58545
- prepareCustomEmoji: (params) => request("POST", `${apiPrefix}/custom-emoji/prepare`, params),
58546
- renameCustomEmoji: (params) => request("POST", `${apiPrefix}/custom-emoji/rename`, params),
58547
- sendThreadReply: (params) => request("POST", `${apiPrefix}/thread-reply`, params),
58548
- sendDm: (params) => request("POST", `${apiPrefix}/dm`, params),
58549
- dmOwner: (params) => request("POST", `${apiPrefix}/dm-owner`, params),
58550
- getVaultValues: (params) => request("POST", `${apiPrefix}/vault-values`, params)
58551
- };
58552
- }
58553
-
58554
- // src/mcpServer.ts
60975
+ init_linzumiApiClient();
58555
60976
  init_mcpConfig();
58556
60977
  init_oauth();
58557
60978
  init_protocol();
58558
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
+ }
58559
60988
  function normalizeCustomEmojiNameForSchema(value) {
58560
60989
  return value.trim().replace(/^:+/, "").replace(/:+$/, "").toLowerCase();
58561
60990
  }
@@ -58578,6 +61007,7 @@ var mcpFlagDefinitions = /* @__PURE__ */ new Map([
58578
61007
  ["thread-id", { kind: "value" }],
58579
61008
  ["format", { kind: "value" }],
58580
61009
  ["command", { kind: "value" }],
61010
+ ["tool-scope", { kind: "value" }],
58581
61011
  ["include-token", { kind: "boolean" }],
58582
61012
  ["help", { kind: "boolean" }]
58583
61013
  ]);
@@ -58617,6 +61047,8 @@ Tools:
58617
61047
  linzumi_get_channel Read bounded recent messages and channel metadata.
58618
61048
  linzumi_get_coding_job_metadata
58619
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.
58620
61052
  linzumi_upsert_coding_job_plan
58621
61053
  Set or update the coding job goal.
58622
61054
  linzumi_replace_coding_job_plan_steps
@@ -58627,6 +61059,12 @@ Tools:
58627
61059
  Link the coding job to its primary GitHub pull request.
58628
61060
  linzumi_list_vault_secrets
58629
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.
58630
61068
  linzumi_send_channel_message
58631
61069
  Send a plain-text message to the scoped channel.
58632
61070
  linzumi_send_thread_reply
@@ -58662,6 +61100,7 @@ async function runMcpServer(args) {
58662
61100
  });
58663
61101
  const ownerUsername = stringValue5(values, "owner-username") ?? process.env.LINZUMI_MCP_OWNER_USERNAME;
58664
61102
  const operatingMode = mcpOperatingMode(stringValue5(values, "mode"));
61103
+ const toolScope = mcpToolScope(stringValue5(values, "tool-scope"));
58665
61104
  const cwd = stringValue5(values, "cwd") ?? process.cwd();
58666
61105
  const defaultThreadId = stringValue5(values, "thread-id") ?? process.env.LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID;
58667
61106
  const client = createLinzumiMcpApiClient({
@@ -58675,318 +61114,447 @@ async function runMcpServer(args) {
58675
61114
  version: "0.1.0"
58676
61115
  });
58677
61116
  registerEmptyMcpResources(server);
58678
- server.tool(
58679
- "linzumi_get_message",
58680
- "Read one Linzumi message by scoped message_seq/message_id or a Linzumi message URL, with optional bounded before/after context.",
58681
- {
58682
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58683
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58684
- message_id: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Message seq inside the scoped channel."),
58685
- message_seq: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Alias for message_id."),
58686
- message_url: external_exports2.string().optional().describe("Full Linzumi message URL with #message-<seq>."),
58687
- before: external_exports2.number().int().min(0).max(100).optional().describe("Number of visible messages before the target."),
58688
- after: external_exports2.number().int().min(0).max(100).optional().describe("Number of visible messages after the target.")
58689
- },
58690
- async (params) => mcpJsonResult(await client.getMessage(params))
58691
- );
58692
- server.tool(
58693
- "linzumi_get_thread",
58694
- "Read one Linzumi thread by thread_id in the scoped channel, including the parent message and bounded replies.",
58695
- {
58696
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58697
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58698
- thread_id: external_exports2.string().uuid().describe("Linzumi thread UUID."),
58699
- limit: external_exports2.number().int().min(1).max(100).optional().describe("Maximum replies to return."),
58700
- before_seq: external_exports2.number().int().positive().optional().describe("Return replies before this channel seq.")
58701
- },
58702
- async (params) => mcpJsonResult(await client.getThread(params))
58703
- );
58704
- server.tool(
58705
- "linzumi_get_channel",
58706
- "Read scoped Linzumi channel metadata and bounded recent channel messages.",
58707
- {
58708
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58709
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58710
- limit: external_exports2.number().int().min(1).max(100).optional().describe("Maximum messages to return."),
58711
- before_seq: external_exports2.number().int().positive().optional().describe("Return messages before this channel seq.")
58712
- },
58713
- async (params) => mcpJsonResult(await client.getChannel(params))
58714
- );
58715
- server.tool(
58716
- "linzumi_get_coding_job_metadata",
58717
- "Read the active coding job workflow metadata for a thread, including the agent-owned goal, ordered plan steps, step lock versions, and workflow status.",
58718
- {
58719
- thread_id: external_exports2.string().uuid().optional().describe(
58720
- "Linzumi thread UUID. Defaults to the active coding job thread."
58721
- )
58722
- },
58723
- async (params) => mcpJsonResult(
58724
- await client.getCodingJobMetadata(
58725
- paramsWithDefaultThread(params, defaultThreadId)
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."
61124
+ ),
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."
61171
+ )
61172
+ },
61173
+ async (params) => mcpJsonResult(
61174
+ await client.getCodingJobMetadata(
61175
+ paramsWithDefaultThread(params, defaultThreadId)
61176
+ )
58726
61177
  )
58727
- )
58728
- );
58729
- server.tool(
58730
- "linzumi_upsert_coding_job_plan",
58731
- "Set or update the current coding job goal. Use this at job start and whenever the goal changes materially.",
58732
- {
58733
- thread_id: external_exports2.string().uuid().optional().describe(
58734
- "Linzumi thread UUID. Defaults to the active coding job thread."
58735
- ),
58736
- goal: external_exports2.string().min(1).max(1e4).describe("Concrete user-visible goal for this coding job.")
58737
- },
58738
- async (params) => mcpJsonResult(
58739
- await client.upsertCodingJobPlan(
58740
- paramsWithDefaultThread(params, defaultThreadId)
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
+ )
58741
61196
  )
58742
- )
58743
- );
58744
- server.tool(
58745
- "linzumi_replace_coding_job_plan_steps",
58746
- "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.",
58747
- {
58748
- thread_id: external_exports2.string().uuid().optional().describe(
58749
- "Linzumi thread UUID. Defaults to the active coding job thread."
58750
- ),
58751
- goal: external_exports2.string().min(1).max(1e4).optional().describe(
58752
- "Required when no plan exists yet; updates the current coding job goal."
58753
- ),
58754
- steps: external_exports2.array(
58755
- external_exports2.object({
58756
- title: external_exports2.string().min(1).max(255),
58757
- description: external_exports2.string().min(1).max(1e4),
58758
- status: external_exports2.enum([
58759
- "pending",
58760
- "active",
58761
- "completed",
58762
- "blocked",
58763
- "canceled"
58764
- ]),
58765
- status_note: external_exports2.string().min(1).max(4e3).optional(),
58766
- completed_debrief: external_exports2.string().min(1).max(2e4).optional()
58767
- })
58768
- ).min(1).max(50).describe(
58769
- "Ordered user-visible plan steps. Completed steps must include completed_debrief."
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
+ )
58770
61215
  )
58771
- },
58772
- async (params) => mcpJsonResult(
58773
- await client.replaceCodingJobPlanSteps(
58774
- paramsWithDefaultThread(params, defaultThreadId)
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
+ )
58775
61253
  )
58776
- )
58777
- );
58778
- server.tool(
58779
- "linzumi_update_coding_job_plan_step",
58780
- "Update one coding job plan step status or note. Read metadata first and pass the current step lock_version.",
58781
- {
58782
- thread_id: external_exports2.string().uuid().optional().describe(
58783
- "Linzumi thread UUID. Defaults to the active coding job thread."
58784
- ),
58785
- step_id: external_exports2.string().uuid().describe("Step id returned by linzumi_get_coding_job_metadata."),
58786
- lock_version: external_exports2.number().int().positive().describe("Current step lock_version from metadata."),
58787
- status: external_exports2.enum(["pending", "active", "completed", "blocked", "canceled"]),
58788
- status_note: external_exports2.string().min(1).max(4e3).optional(),
58789
- completed_debrief: external_exports2.string().min(1).max(2e4).optional().describe("Required when marking a step completed.")
58790
- },
58791
- async (params) => mcpJsonResult(
58792
- await client.updateCodingJobPlanStep(
58793
- paramsWithDefaultThread(params, defaultThreadId)
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
+ )
58794
61282
  )
58795
- )
58796
- );
58797
- server.tool(
58798
- "linzumi_link_coding_job_pull_request",
58799
- "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.",
58800
- {
58801
- thread_id: external_exports2.string().uuid().optional().describe(
58802
- "Linzumi thread UUID. Defaults to the active coding job thread."
58803
- ),
58804
- github_repo_owner: external_exports2.string().min(1).max(255),
58805
- github_repo_name: external_exports2.string().min(1).max(255),
58806
- github_pr_number: external_exports2.number().int().positive(),
58807
- github_pr_url: external_exports2.string().url(),
58808
- title: external_exports2.string().min(1).max(255),
58809
- body: external_exports2.string().min(1).max(1e5).optional(),
58810
- head_branch: external_exports2.string().min(1).max(255),
58811
- head_sha: external_exports2.string().min(7).max(64),
58812
- base_branch: external_exports2.string().min(1).max(255),
58813
- base_sha: external_exports2.string().min(7).max(64),
58814
- state: external_exports2.enum(["draft", "open", "closed", "merged"]).optional(),
58815
- is_draft: external_exports2.boolean().optional(),
58816
- review_status: external_exports2.enum(["unknown", "review_required", "approved", "changes_requested"]).optional(),
58817
- source_remote_url: external_exports2.string().min(1).max(1e3).describe(
58818
- "Git remote URL for the local job worktree; required when this job has no repo workspace yet."
58819
- ),
58820
- runtime_worktree_root: external_exports2.string().min(1).max(1e3).describe(
58821
- "Absolute local worktree path for the coding job; required when this job has no repo workspace yet."
58822
- ),
58823
- dirty_status: external_exports2.enum(["unknown", "clean", "dirty"]).optional(),
58824
- dirty_summary: external_exports2.string().min(1).max(2e3).optional()
58825
- },
58826
- async (params) => mcpJsonResult(
58827
- await client.linkCodingJobPullRequest(
58828
- paramsWithDefaultThread(params, defaultThreadId)
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
+ )
58829
61321
  )
58830
- )
58831
- );
58832
- server.tool(
58833
- "linzumi_upload_files",
58834
- "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.",
58835
- {
58836
- target: external_exports2.object({
58837
- kind: external_exports2.enum(["new_message", "message_seq", "latest_assistant_message"]).describe(
58838
- "new_message creates a message; message_seq edits an exact message; latest_assistant_message edits the latest assistant message in the scoped thread/channel."
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."
58839
61384
  ),
58840
- message_seq: external_exports2.union([external_exports2.string(), external_exports2.number()]).optional().describe("Required when kind is message_seq."),
58841
- thread_id: external_exports2.string().optional().describe(
58842
- "Thread UUID. Defaults to the active Linzumi thread when the MCP server was launched with one."
61385
+ workspace: external_exports2.string().optional().describe(
61386
+ "Workspace slug. Omit to use the authenticated token scope."
58843
61387
  )
58844
- }).describe(
58845
- "Attachment target. Do not guess; use new_message when there is no previous assistant message."
58846
- ),
58847
- files: external_exports2.array(external_exports2.string().min(1)).min(1).max(16).describe(
58848
- "Local file paths to upload. Paths must be non-hidden supported file types inside the approved workspace or Downloads."
58849
- ),
58850
- body: external_exports2.string().min(1).max(2e4).optional().describe(
58851
- "Required for new_message. Optional replacement body for message_seq/latest_assistant_message; when omitted, the existing message body is preserved."
58852
- ),
58853
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58854
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope.")
58855
- },
58856
- async (params) => {
58857
- const target = targetWithDefaultThread(
58858
- params.target,
58859
- defaultThreadId
58860
- );
58861
- const result = await uploadFilesWithClient({
58862
- client,
58863
- cwd,
58864
- kandanUrl,
58865
- target,
58866
- files: params.files,
58867
- workspace: params.workspace,
58868
- channel: params.channel,
58869
- body: params.body
58870
- });
58871
- return mcpJsonResult(result);
58872
- }
58873
- );
58874
- server.tool(
58875
- "linzumi_list_vault_secrets",
58876
- "List vault secret names, descriptions, and whether each secret is personal or workspace-owned. Does not return secret values.",
58877
- {},
58878
- async (params) => mcpJsonResult(await client.listVaultSecrets(params))
58879
- );
58880
- server.tool(
58881
- "linzumi_upload_custom_emoji",
58882
- "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:.",
58883
- {
58884
- name: customEmojiNameSchema.describe(
58885
- "Custom emoji shortcode name, with or without surrounding colons."
58886
- ),
58887
- file: external_exports2.string().min(1).describe(
58888
- "Local PNG/WebP/GIF/JPEG file path to upload. Prefer transparent PNG for generated emoji."
58889
- ),
58890
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope.")
58891
- },
58892
- async (params) => mcpJsonResult(
58893
- await uploadCustomEmojiWithClient({
58894
- client,
58895
- cwd,
58896
- kandanUrl,
58897
- name: params.name,
58898
- file: params.file,
58899
- workspace: params.workspace
58900
- })
58901
- )
58902
- );
58903
- server.tool(
58904
- "linzumi_rename_custom_emoji",
58905
- "Rename an existing workspace custom emoji. The new name may include surrounding colons; Linzumi stores the normalized shortcode without colons.",
58906
- {
58907
- emoji_id: external_exports2.union([external_exports2.string(), external_exports2.number().int().positive()]).describe(
58908
- "Workspace custom emoji id returned by linzumi_upload_custom_emoji or workspace emoji listings."
58909
- ),
58910
- name: customEmojiNameSchema.describe(
58911
- "New shortcode name, with or without surrounding colons."
58912
- ),
58913
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope.")
58914
- },
58915
- async (params) => mcpJsonResult(await client.renameCustomEmoji(params))
58916
- );
58917
- server.tool(
58918
- "linzumi_dm_owner",
58919
- "Send a plain-text DM to the configured Commander owner. Requires dm.write and a visible owner username.",
58920
- {
58921
- owner_username: external_exports2.string().optional().describe(
58922
- "Owner username. Defaults to LINZUMI_MCP_OWNER_USERNAME or --owner-username."
58923
- ),
58924
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58925
- channel: external_exports2.string().optional().describe("Source channel slug used to verify owner visibility."),
58926
- body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to DM.")
58927
- },
58928
- async (params) => {
58929
- const owner = params.owner_username ?? ownerUsername;
58930
- if (owner === void 0) {
58931
- throw new Error("owner_username is required for linzumi_dm_owner");
58932
- }
58933
- return mcpJsonResult(
58934
- await client.dmOwner({
58935
- ...params,
58936
- owner_username: owner
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
58937
61397
  })
58938
- );
58939
- }
58940
- );
58941
- server.tool(
58942
- "linzumi_send_channel_message",
58943
- "Send a plain-text message as the authenticated local-runner user to the scoped Linzumi channel. Requires channel.write.",
58944
- {
58945
- workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58946
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58947
- body: external_exports2.string().min(1).max(2e4).describe("Plain-text message body to post.")
58948
- },
58949
- async (params) => mcpJsonResult(await client.sendChannelMessage(params))
58950
- );
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
+ }
58951
61417
  server.tool(
58952
- "linzumi_send_thread_reply",
58953
- "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.",
58954
61420
  {
58955
61421
  workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58956
- channel: external_exports2.string().optional().describe("Channel slug. Omit to use the authenticated token scope."),
58957
- thread_id: external_exports2.string().uuid().describe("Existing Linzumi thread UUID."),
58958
- 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.")
58959
61428
  },
58960
- async (params) => mcpJsonResult(await client.sendThreadReply(params))
61429
+ async (params) => mcpJsonResult(await client.noteProjectDirectory(params))
58961
61430
  );
58962
61431
  server.tool(
58963
- "linzumi_send_dm",
58964
- "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.",
58965
61434
  {
58966
61435
  workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58967
- channel: external_exports2.string().optional().describe("Source channel slug used to verify target visibility."),
58968
- username: external_exports2.string().describe("Visible Linzumi username to DM."),
58969
- 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
+ )
58970
61445
  },
58971
- async (params) => mcpJsonResult(await client.sendDm(params))
61446
+ async (params) => mcpJsonResult(await client.noteAgentConversation(params))
58972
61447
  );
58973
61448
  server.tool(
58974
- "linzumi_get_vault_values",
58975
- "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.",
58976
61451
  {
58977
61452
  workspace: external_exports2.string().optional().describe("Workspace slug. Omit to use the authenticated token scope."),
58978
- channel: external_exports2.string().optional().describe(
58979
- "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."
61458
+ ),
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."
58980
61462
  ),
58981
- thread_id: external_exports2.string().uuid().describe("Existing Linzumi thread UUID for the inline approval."),
58982
- env_var_names: external_exports2.array(external_exports2.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/)).min(1).max(20).describe(
58983
- "Explicit vault env var names to request. This tool cannot list all vault keys."
61463
+ current_source: external_exports2.string().optional().describe(
61464
+ "Short machine-readable source label such as git, codex, or claude_code."
58984
61465
  ),
58985
- description: external_exports2.string().min(1).max(500).describe("Human-readable reason shown in the approval request."),
58986
- justification: external_exports2.string().min(10).max(1e3).describe("Agent justification shown to the approver.")
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.")
58987
61468
  },
58988
- async (params) => mcpJsonResult(await client.getVaultValues(params))
61469
+ async (params) => mcpJsonResult(
61470
+ await client.noteOnboardingDiscoveryStatus(params)
61471
+ )
58989
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
+ }
58990
61558
  await server.connect(new StdioServerTransport());
58991
61559
  }
58992
61560
  async function uploadFilesWithClient(args) {
@@ -59014,13 +61582,13 @@ async function uploadFilesWithClient(args) {
59014
61582
  if (uploadUrl === void 0) {
59015
61583
  throw new Error("Linzumi upload prepare response missing upload_url");
59016
61584
  }
59017
- const bytes = await readFile2(file.path);
61585
+ const bytes = await readFile3(file.path);
59018
61586
  const uploadBody = bytes.buffer.slice(
59019
61587
  bytes.byteOffset,
59020
61588
  bytes.byteOffset + bytes.byteLength
59021
61589
  );
59022
61590
  const response = await fetch(
59023
- resolveLinzumiUploadUrl(args.kandanUrl, uploadUrl),
61591
+ resolveLinzumiUploadUrl2(args.kandanUrl, uploadUrl),
59024
61592
  {
59025
61593
  method: uploadMethod,
59026
61594
  headers: { "content-type": file.contentType },
@@ -59064,13 +61632,13 @@ async function uploadCustomEmojiWithClient(args) {
59064
61632
  if (uploadUrl === void 0) {
59065
61633
  throw new Error("Linzumi custom emoji prepare response missing upload_url");
59066
61634
  }
59067
- const bytes = await readFile2(file.path);
61635
+ const bytes = await readFile3(file.path);
59068
61636
  const uploadBody = bytes.buffer.slice(
59069
61637
  bytes.byteOffset,
59070
61638
  bytes.byteOffset + bytes.byteLength
59071
61639
  );
59072
61640
  const response = await fetch(
59073
- resolveLinzumiUploadUrl(args.kandanUrl, uploadUrl),
61641
+ resolveLinzumiUploadUrl2(args.kandanUrl, uploadUrl),
59074
61642
  {
59075
61643
  method: uploadMethod,
59076
61644
  headers: { "content-type": file.contentType },
@@ -59133,7 +61701,7 @@ function paramsWithDefaultThread(params, defaultThreadId) {
59133
61701
  }
59134
61702
  return { ...params, thread_id: defaultThreadId };
59135
61703
  }
59136
- function resolveLinzumiUploadUrl(kandanUrl, uploadUrl) {
61704
+ function resolveLinzumiUploadUrl2(kandanUrl, uploadUrl) {
59137
61705
  try {
59138
61706
  return new URL(uploadUrl).toString();
59139
61707
  } catch (_error) {
@@ -59171,7 +61739,8 @@ async function runMcpConfig(args) {
59171
61739
  accessToken: token,
59172
61740
  delegationAuthFilePath: stringValue5(values, "delegation-auth-file"),
59173
61741
  ownerUsername: stringValue5(values, "owner-username") ?? process.env.LINZUMI_MCP_OWNER_USERNAME,
59174
- operatingMode
61742
+ operatingMode,
61743
+ toolScope: mcpToolScope(stringValue5(values, "tool-scope"))
59175
61744
  });
59176
61745
  switch (format) {
59177
61746
  case "codex":
@@ -59299,6 +61868,17 @@ function mcpOperatingMode(value) {
59299
61868
  throw new Error("--mode must be voice or text");
59300
61869
  }
59301
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
+ }
59302
61882
  function required(values, key) {
59303
61883
  const value = stringValue5(values, key);
59304
61884
  if (value === void 0) {
@@ -59641,6 +62221,7 @@ function optionalObjectField(input, key) {
59641
62221
 
59642
62222
  // src/index.ts
59643
62223
  init_userFacingErrors();
62224
+ var onboardingDiscoveryRequiredScopes = ["local_runner.discovery.write"];
59644
62225
  var flagDefinitions = /* @__PURE__ */ new Map([
59645
62226
  ["version", { kind: "boolean" }],
59646
62227
  ["api-url", { kind: "value" }],
@@ -59664,6 +62245,7 @@ var flagDefinitions = /* @__PURE__ */ new Map([
59664
62245
  ["forward-port", { kind: "value" }],
59665
62246
  ["code-server-bin", { kind: "value" }],
59666
62247
  ["fast", { kind: "boolean" }],
62248
+ ["no-onboarding-discovery", { kind: "boolean" }],
59667
62249
  ["log-file", { kind: "value" }],
59668
62250
  ["status-dir", { kind: "value" }],
59669
62251
  ["timeout-ms", { kind: "value" }],
@@ -59786,8 +62368,41 @@ async function main(args) {
59786
62368
  await runThreadCodexWorker(parseThreadCodexWorkerArgs(parsed.args));
59787
62369
  return;
59788
62370
  }
59789
- const options = await parseRunnerArgs(parsed.args);
59790
- 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
+ }
59791
62406
  return;
59792
62407
  }
59793
62408
  }
@@ -60123,6 +62738,7 @@ async function parseStartRunnerArgs(args, deps = {
60123
62738
  const allowedCwds = Array.from(/* @__PURE__ */ new Set([cwd, ...explicitAllowedCwds]));
60124
62739
  const requestedCodexBin = stringValue7(values, "codex-bin") ?? "codex";
60125
62740
  const customCodeServerBin = stringValue7(values, "code-server-bin");
62741
+ const onboardingDiscovery = values.get("no-onboarding-discovery") === true ? void 0 : "start";
60126
62742
  const initialDependencyStatus = await deps.buildDependencyStatus({
60127
62743
  cwd,
60128
62744
  codexBin: requestedCodexBin,
@@ -60141,9 +62757,10 @@ async function parseStartRunnerArgs(args, deps = {
60141
62757
  const token = await deps.resolveToken({
60142
62758
  kandanUrl,
60143
62759
  explicitToken,
60144
- onboarding: "start",
62760
+ onboarding: onboardingDiscovery === "start" ? "start" : void 0,
60145
62761
  authFilePath,
60146
62762
  callbackHost,
62763
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0,
60147
62764
  reportRejectedCachedToken
60148
62765
  });
60149
62766
  const target = await deps.fetchStartTarget({ kandanUrl, accessToken: token });
@@ -60151,7 +62768,8 @@ async function parseStartRunnerArgs(args, deps = {
60151
62768
  kandanUrl,
60152
62769
  accessToken: token,
60153
62770
  workspaceSlug: target.workspaceSlug,
60154
- channelSlug: target.channelSlug
62771
+ channelSlug: target.channelSlug,
62772
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0
60155
62773
  });
60156
62774
  const targetToken = tokenMatchesTarget ? token : await resolveStartTargetToken({
60157
62775
  kandanUrl,
@@ -60159,6 +62777,7 @@ async function parseStartRunnerArgs(args, deps = {
60159
62777
  target,
60160
62778
  authFilePath,
60161
62779
  callbackHost,
62780
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0,
60162
62781
  resolveToken: deps.resolveToken,
60163
62782
  reportRejectedCachedToken
60164
62783
  });
@@ -60200,6 +62819,7 @@ async function parseStartRunnerArgs(args, deps = {
60200
62819
  dependencyStatus,
60201
62820
  claudeCodeAvailable,
60202
62821
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
62822
+ onboardingDiscovery,
60203
62823
  channelSession: {
60204
62824
  workspaceSlug: target.workspaceSlug,
60205
62825
  channelSlug: target.channelSlug,
@@ -60309,7 +62929,7 @@ async function parseAgentRunnerArgs(args, deps = {
60309
62929
  };
60310
62930
  }
60311
62931
  function readAgentTokenTextFile(path2) {
60312
- return existsSync14(path2) ? readFileSync16(path2, "utf8") : void 0;
62932
+ return existsSync16(path2) ? readFileSync19(path2, "utf8") : void 0;
60313
62933
  }
60314
62934
  function rejectAgentRunnerTargetingFlags(values) {
60315
62935
  const unsupportedFlags = [
@@ -60381,6 +63001,7 @@ async function resolveStartTargetToken(args) {
60381
63001
  channelSlug: args.target.channelSlug,
60382
63002
  authFilePath: args.authFilePath,
60383
63003
  callbackHost: args.callbackHost,
63004
+ requiredScopes: args.requiredScopes,
60384
63005
  reportRejectedCachedToken: args.reportRejectedCachedToken
60385
63006
  });
60386
63007
  }
@@ -60420,6 +63041,7 @@ async function parseRunnerArgs(args, deps = {
60420
63041
  ) : [...localConfiguredAllowedCwds.allowedCwds];
60421
63042
  const requestedCodexBin = stringValue7(values, "codex-bin") ?? "codex";
60422
63043
  const customCodeServerBin = stringValue7(values, "code-server-bin");
63044
+ const onboardingDiscovery = values.get("no-onboarding-discovery") === true ? void 0 : "start";
60423
63045
  const initialDependencyStatus = await deps.buildDependencyStatus({
60424
63046
  cwd,
60425
63047
  codexBin: requestedCodexBin,
@@ -60434,6 +63056,7 @@ async function parseRunnerArgs(args, deps = {
60434
63056
  workspaceSlug,
60435
63057
  authFilePath: stringValue7(values, "auth-file"),
60436
63058
  callbackHost: stringValue7(values, "oauth-callback-host"),
63059
+ requiredScopes: onboardingDiscovery === "start" ? onboardingDiscoveryRequiredScopes : void 0,
60437
63060
  reportRejectedCachedToken: () => {
60438
63061
  process.stderr.write(
60439
63062
  "Cached Linzumi local runner auth was rejected; starting OAuth.\n"
@@ -60481,6 +63104,7 @@ async function parseRunnerArgs(args, deps = {
60481
63104
  claudeCodeAvailable,
60482
63105
  workspaceSlug: workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
60483
63106
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
63107
+ onboardingDiscovery,
60484
63108
  channelSession: void 0
60485
63109
  };
60486
63110
  }
@@ -60496,6 +63120,15 @@ function runnerRuntimeDefaultsFromValues(values) {
60496
63120
  function electronAutoConnectLaunchSource() {
60497
63121
  return process.env.LINZUMI_ELECTRON_AUTO_CONNECT_RUNNER === "1" ? "electron_auto_connect" : void 0;
60498
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
+ }
60499
63132
  function kandanUrlValue(values) {
60500
63133
  const apiUrl = stringValue7(values, "api-url");
60501
63134
  const linzumiUrl = stringValue7(values, "linzumi-url");
@@ -60607,10 +63240,10 @@ function rejectStartTargetingFlags(values) {
60607
63240
  }
60608
63241
  function resolveUserPath(pathValue) {
60609
63242
  if (pathValue === "~") {
60610
- return homedir13();
63243
+ return homedir15();
60611
63244
  }
60612
63245
  if (pathValue.startsWith("~/")) {
60613
- return resolve10(homedir13(), pathValue.slice(2));
63246
+ return resolve10(homedir15(), pathValue.slice(2));
60614
63247
  }
60615
63248
  return resolve10(pathValue);
60616
63249
  }