larkway 0.3.7 → 0.3.9

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.
package/dist/main.js CHANGED
@@ -107360,7 +107360,6 @@ var require_main = __commonJS({
107360
107360
  });
107361
107361
 
107362
107362
  // src/main.ts
107363
- import { execSync } from "node:child_process";
107364
107363
  import { mkdirSync } from "node:fs";
107365
107364
  import path13 from "node:path";
107366
107365
 
@@ -111527,20 +111526,6 @@ var ChatEntry = external_exports.object({
111527
111526
  purpose: external_exports.enum(["production", "test", "staging"]).default("production"),
111528
111527
  description: external_exports.string().optional()
111529
111528
  });
111530
- var CentralConfig = external_exports.object({
111531
- /**
111532
- * Git URL or local path of the central config repo (where `bots/` lives).
111533
- * e.g. "git@gitlab.company.com:ops/larkway-bots.git" or a bare repo path.
111534
- */
111535
- repo: external_exports.string().min(1, "centralConfig.repo is required (git url or path)"),
111536
- /** Branch to track on the central repo. @default "main" */
111537
- branch: external_exports.string().min(1).default("main"),
111538
- /**
111539
- * Path INSIDE the central repo that holds the bot files (<id>.yaml +
111540
- * <id>.memory.md). @default "bots"
111541
- */
111542
- path: external_exports.string().min(1).default("bots")
111543
- });
111544
111529
  var ConfigJson = external_exports.object({
111545
111530
  conventions: ConventionsConfig,
111546
111531
  permissions: PermissionsConfig,
@@ -111548,12 +111533,7 @@ var ConfigJson = external_exports.object({
111548
111533
  * List of allowed chats with labels. If empty/missing, bridge falls back
111549
111534
  * to the legacy `LARK_ALLOWED_CHAT_IDS` env var (deprecated).
111550
111535
  */
111551
- chats: external_exports.array(ChatEntry).default([]),
111552
- /**
111553
- * Central config repo (V2.2 §7 A.2). Optional — when present, `larkway sync`
111554
- * pulls bots/ from it. Absent = local self-management.
111555
- */
111556
- centralConfig: CentralConfig.optional()
111536
+ chats: external_exports.array(ChatEntry).default([])
111557
111537
  });
111558
111538
  var DEFAULT_CONFIG_PATH = path2.join(larkwayHome(), "config.json");
111559
111539
  function getLanIp() {
@@ -111577,8 +111557,7 @@ function defaultConfigJson() {
111577
111557
  portRangeEnd: 3050
111578
111558
  },
111579
111559
  permissions: { allowExtra: [] },
111580
- chats: [],
111581
- centralConfig: void 0
111560
+ chats: []
111582
111561
  };
111583
111562
  }
111584
111563
  async function loadConfigJson(configPath = DEFAULT_CONFIG_PATH) {
@@ -112549,9 +112528,11 @@ ${recent.join("\n")}` : recent.join("\n");
112549
112528
  });
112550
112529
  elements.push({ tag: "hr" });
112551
112530
  }
112531
+ const mentionPrefix = atMentionMarkdown(opts.mentionOpenId);
112552
112532
  const cleanBody = opts.bodyText ? stripLeakedToolMarkup(opts.bodyText) : "";
112553
- if (cleanBody) {
112554
- const chunks = chunkMarkdown(cleanBody);
112533
+ const bodyWithMention = [mentionPrefix, cleanBody].filter(Boolean).join("\n\n");
112534
+ if (bodyWithMention) {
112535
+ const chunks = chunkMarkdown(bodyWithMention);
112555
112536
  for (let i = 0; i < chunks.length; i++) {
112556
112537
  elements.push({
112557
112538
  tag: "markdown",
@@ -112593,6 +112574,11 @@ ${chunks[i]}`
112593
112574
  };
112594
112575
  return JSON.stringify(card);
112595
112576
  }
112577
+ function atMentionMarkdown(userId) {
112578
+ if (!userId) return "";
112579
+ if (!/^[A-Za-z0-9_:-]+$/.test(userId)) return "";
112580
+ return `<at id=${userId}></at>`;
112581
+ }
112596
112582
  var CardHandleImpl = class {
112597
112583
  messageId;
112598
112584
  /** Transport for the leaf PATCH network call (default = lark-cli). */
@@ -112644,6 +112630,7 @@ var CardHandleImpl = class {
112644
112630
  showToolSummary: this.showToolSummary,
112645
112631
  status: cardStatus,
112646
112632
  failureReason: opts.failureReason,
112633
+ mentionOpenId: opts.mentionOpenId,
112647
112634
  // V2 dynamic-choice buttons — agent-declared, only on finalize.
112648
112635
  choices: opts.choices,
112649
112636
  choicePrompt: opts.choicePrompt,
@@ -113373,6 +113360,33 @@ function renderTurnTakingHint(limit) {
113373
113360
  "</turn-taking>"
113374
113361
  ];
113375
113362
  }
113363
+ function renderRuntimeWarningsBlock(warnings) {
113364
+ if (warnings.length === 0) return [];
113365
+ const hasMissingLarkCli = warnings.some((warning) => warning.command === "lark-cli");
113366
+ return [
113367
+ "<runtime-warnings>",
113368
+ "Bridge \u68C0\u6D4B\u5230\u4EE5\u4E0B\u672C\u673A\u80FD\u529B\u6682\u4E0D\u53EF\u7528\u3002\u8FD9\u662F\u63D0\u793A,\u4E0D\u662F\u5F3A\u5236\u505C\u6B62\u6761\u4EF6:",
113369
+ ...warnings.map((warning) => {
113370
+ const name = warning.command ? `${warning.label} (${warning.command})` : warning.label;
113371
+ const reason = warning.reason ? `: ${warning.reason}` : "";
113372
+ const installHint = warning.installHint ? ` Fix hint: ${warning.installHint}` : "";
113373
+ return `- ${name}${reason}${installHint}`;
113374
+ }),
113375
+ "",
113376
+ "\u5904\u7406\u539F\u5219:",
113377
+ "- \u80FD\u4EC5\u51ED\u5F53\u524D\u6D88\u606F\u7EE7\u7EED\u7684\u4EFB\u52A1,\u7EE7\u7EED\u5904\u7406,\u4E0D\u8981\u56E0\u4E3A warning \u76F4\u63A5\u62D2\u7EDD\u3002",
113378
+ "- \u53EA\u6709\u5F53\u4EFB\u52A1\u786E\u5B9E\u9700\u8981\u7F3A\u5931\u80FD\u529B\u65F6,\u518D\u5728 last_message \u91CC\u7528\u4EA7\u54C1\u5316\u8BED\u8A00\u544A\u8BC9\u7528\u6237\u7F3A\u4EC0\u4E48\u3001\u4F1A\u5F71\u54CD\u4EC0\u4E48\u3001\u5982\u4F55\u7EE7\u7EED\u3002",
113379
+ ...hasMissingLarkCli ? [
113380
+ "- \u5BF9\u7F3A\u5C11 lark-cli \u7684\u60C5\u51B5:\u4E0D\u8981\u989D\u5916 @ \u7528\u6237;\u5728\u5361\u7247\u91CC\u8F7B\u91CF\u8BF4\u660E\u5F53\u524D\u65E0\u6CD5\u81EA\u52A8\u8BFB\u53D6\u98DE\u4E66\u8BDD\u9898\u5386\u53F2\u3001\u9644\u4EF6\u6216\u6587\u6863\u5373\u53EF\u3002",
113381
+ '- \u5982\u679C\u5F53\u524D\u4EFB\u52A1\u9700\u8981\u8FD9\u4E9B\u4E0A\u4E0B\u6587,\u7528 choices \u95EE\u662F\u5426\u5141\u8BB8\u5B89\u88C5\u6700\u65B0\u7248\u98DE\u4E66 CLI\u3002\u5EFA\u8BAE: `choice_prompt: "\u8BFB\u53D6\u98DE\u4E66\u5386\u53F2\u9700\u8981\u672C\u673A\u5B89\u88C5\u6700\u65B0\u7248\u98DE\u4E66 CLI,\u662F\u5426\u5141\u8BB8\u6211\u5C1D\u8BD5\u5B89\u88C5?"`, `choices: [{label:"\u5141\u8BB8\u5B89\u88C5", value:"\u5141\u8BB8\u5B89\u88C5 lark-cli"}, {label:"\u5148\u4E0D\u5B89\u88C5", value:"\u5148\u4E0D\u5B89\u88C5 lark-cli,\u6211\u4F1A\u628A\u8981\u5904\u7406\u7684\u5185\u5BB9\u8D34\u5230\u8BDD\u9898\u91CC"}]`\u3002',
113382
+ "- \u7528\u6237\u660E\u786E\u9009\u62E9/\u56DE\u590D\u5141\u8BB8\u5B89\u88C5\u540E,\u518D\u5C1D\u8BD5\u5B89\u88C5;\u4E0D\u8981\u5728\u672A\u786E\u8BA4\u524D\u6539\u5BBF\u4E3B\u673A\u5168\u5C40\u73AF\u5883\u3002",
113383
+ "- \u63A8\u8350\u5B89\u88C5\u547D\u4EE4: `npx -y @larksuite/cli@latest install`,\u7136\u540E\u8FD0\u884C `lark-cli --version` \u9A8C\u8BC1\u3002",
113384
+ '- \u5982\u9047 npm \u5168\u5C40\u76EE\u5F55\u6743\u9650\u9519\u8BEF(EACCES/permission denied),\u4F7F\u7528\u7528\u6237\u7EA7 prefix \u540E\u91CD\u8BD5: `mkdir -p ~/.npm-global && npm config set prefix "$HOME/.npm-global" && export PATH="$HOME/.npm-global/bin:$PATH" && npx -y @larksuite/cli@latest install`\u3002\u4E0D\u8981\u9ED8\u8BA4\u8981\u6C42 sudo\u3002',
113385
+ "- \u5B89\u88C5\u6210\u529F\u540E,\u5982\u679C\u672C\u8F6E\u9700\u8981\u7ACB\u5373\u8BFB\u53D6\u98DE\u4E66\u4E0A\u4E0B\u6587,\u53EF\u5728\u5F53\u524D shell \u4E2D\u5E26\u4E0A\u4FEE\u590D\u540E\u7684 PATH \u7EE7\u7EED\u5C1D\u8BD5;\u82E5\u9700\u8981 bridge \u540E\u7EED\u8F6E\u6B21\u7A33\u5B9A\u4F7F\u7528,\u8BF7\u63D0\u793A owner \u91CD\u542F Larkway\u3002"
113386
+ ] : [],
113387
+ "</runtime-warnings>"
113388
+ ];
113389
+ }
113376
113390
  function renderWorkspaceBlock(primarySlug, primaryCachePath, defaultBranch, extraRepos) {
113377
113391
  const lines = [
113378
113392
  "<workspace>",
@@ -113443,7 +113457,17 @@ function sceneFacts(parsed, isNewThread) {
113443
113457
  };
113444
113458
  }
113445
113459
  function renderPrompt(input) {
113446
- const { parsed, isNewThread, conventions, peers, turn_taking_limit, agentMemory, extraRepoPaths, larkCliProfile } = input;
113460
+ const {
113461
+ parsed,
113462
+ isNewThread,
113463
+ conventions,
113464
+ peers,
113465
+ turn_taking_limit,
113466
+ agentMemory,
113467
+ extraRepoPaths,
113468
+ larkCliProfile,
113469
+ runtimeWarnings = []
113470
+ } = input;
113447
113471
  const backend = input.backend ?? "claude";
113448
113472
  const profileFlag = larkCliProfile ? ` --profile ${larkCliProfile}` : "";
113449
113473
  const attachmentKeys = parsed.attachments.map((a) => a.fileKey);
@@ -113456,6 +113480,7 @@ function renderPrompt(input) {
113456
113480
  const stateContract = renderStateContract(conventions.stateFilePath);
113457
113481
  const peersBlock = peers && peers.length > 0 ? renderPeersBlock(peers) : [];
113458
113482
  const turnTakingBlock = turn_taking_limit && turn_taking_limit > 0 ? renderTurnTakingHint(turn_taking_limit) : [];
113483
+ const runtimeWarningsBlock = renderRuntimeWarningsBlock(runtimeWarnings);
113459
113484
  const extraRepos = extraRepoPaths ?? conventions.extraRepoPaths ?? [];
113460
113485
  const workspaceBlock = isAgentWorkspace ? renderAgentWorkspaceBlock(conventions, extraRepos) : hasRepo ? renderWorkspaceBlock(
113461
113486
  conventions.defaultProjectSlug ?? "repo",
@@ -113511,6 +113536,7 @@ function renderPrompt(input) {
113511
113536
  "",
113512
113537
  ...agentMemoryBlock,
113513
113538
  ...skillIntroNew,
113539
+ ...runtimeWarningsBlock.length > 0 ? [...runtimeWarningsBlock, ""] : [],
113514
113540
  "<thread-context>",
113515
113541
  `thread_id: ${parsed.threadId}`,
113516
113542
  `message_id: ${parsed.messageId}`,
@@ -113555,7 +113581,7 @@ function renderPrompt(input) {
113555
113581
  ` lark-cli docs +get <doc-url>${profileFlag}`,
113556
113582
  "- \u53D6\u672C\u6761\u6D88\u606F\u7684\u9644\u4EF6/\u5185\u8054\u56FE(post \u5185\u8054\u56FE\u4E0D\u5728\u4E0A\u9762 attachments/images \u91CC)\u3001\u62C9\u8BDD\u9898\u5386\u53F2:",
113557
113583
  attachmentHelpLine,
113558
- "- glab / git / gitlab API",
113584
+ "- glab / gh / git API",
113559
113585
  "- pnpm / npm",
113560
113586
  "",
113561
113587
  "**\u91CD\u8981**:`thread_id` \u5C31\u662F\u8BDD\u9898\u9996\u697C\u7684 message_id\u3002\u5982\u679C\u5F53\u524D\u6D88\u606F attachments/feishu_doc_links \u4E3A\u7A7A,\u8BF4\u660E\u8FD0\u8425\u628A\u7D20\u6750\u653E\u5728\u9996\u697C,**\u5148\u62C9\u9996\u697C\u770B\u8FD0\u8425\u539F\u59CB\u9700\u6C42**,\u518D\u51B3\u5B9A\u4E0B\u4E00\u6B65\u3002",
@@ -113578,6 +113604,7 @@ function renderPrompt(input) {
113578
113604
  return [
113579
113605
  ...agentMemoryBlock,
113580
113606
  ...skillIntroCont,
113607
+ ...runtimeWarningsBlock.length > 0 ? [...runtimeWarningsBlock, ""] : [],
113581
113608
  "<thread-context>",
113582
113609
  `thread_id: ${parsed.threadId}`,
113583
113610
  `message_id: ${parsed.messageId}`,
@@ -113850,7 +113877,7 @@ function renderPermissionsRequest(input) {
113850
113877
  "## Repo Pointers",
113851
113878
  "",
113852
113879
  ...repoLines,
113853
- input.bot.gitlab_token_env ? `- GitLab token env name available after human setup: ${input.bot.gitlab_token_env}` : "- GitLab token env name: pending human confirmation",
113880
+ input.bot.gitlab_token_env ? `- Git token env name available after human setup: ${input.bot.gitlab_token_env}` : "- Git token env name: pending human confirmation",
113854
113881
  "",
113855
113882
  "## Human Gate",
113856
113883
  "",
@@ -113876,11 +113903,11 @@ function defaultWorkspacePermissionItems(input) {
113876
113903
  }
113877
113904
  if (input.repos && input.repos.length > 0) {
113878
113905
  for (const repo of input.repos) {
113879
- capabilities.push(`GitLab repo pointer: ${repo.slug} (${repo.branch ?? "master"})`);
113906
+ capabilities.push(`Git repo pointer: ${repo.slug} (${repo.branch ?? "master"})`);
113880
113907
  }
113881
113908
  }
113882
113909
  if (input.bot.gitlab_token_env) {
113883
- capabilities.push(`GitLab token env name: ${input.bot.gitlab_token_env}`);
113910
+ capabilities.push(`Git token env name: ${input.bot.gitlab_token_env}`);
113884
113911
  }
113885
113912
  capabilities.push("Local shell inside the Agent Workspace for task execution and verification");
113886
113913
  return permissionItemsFromCapabilities(capabilities);
@@ -113936,10 +113963,10 @@ function renderDefaultPermissionGrantLines(input) {
113936
113963
  lines.push(`- type=read Feishu chat allowlist: ${chats.join(", ")} source=saved-agent-config`);
113937
113964
  }
113938
113965
  for (const repo of input.repos ?? []) {
113939
- lines.push(`- type=read GitLab repo pointer: ${repo.slug} (${repo.branch ?? "master"}) source=saved-agent-config`);
113966
+ lines.push(`- type=read Git repo pointer: ${repo.slug} (${repo.branch ?? "master"}) source=saved-agent-config`);
113940
113967
  }
113941
113968
  if (input.bot.gitlab_token_env) {
113942
- lines.push(`- type=write GitLab write/MR env=${input.bot.gitlab_token_env} source=saved-agent-config`);
113969
+ lines.push(`- type=write Git write/MR env=${input.bot.gitlab_token_env} source=saved-agent-config`);
113943
113970
  }
113944
113971
  const humanGates = input.humanGates && input.humanGates.length > 0 ? input.humanGates : ["Deploy/restart and production-impact actions require explicit human confirmation."];
113945
113972
  for (const gate of humanGates) {
@@ -114461,6 +114488,11 @@ var BridgeHandler = class {
114461
114488
  constructor(deps) {
114462
114489
  this.deps = deps;
114463
114490
  }
114491
+ runtimeWarnings() {
114492
+ return (this.deps.runtimeRequirements ?? []).filter(
114493
+ (req) => !req.ok && (req.severity === "required" || req.kind === "secret")
114494
+ );
114495
+ }
114464
114496
  /**
114465
114497
  * Enter the main loop: for-await over client.events(), per-thread concurrent dispatch.
114466
114498
  *
@@ -114596,7 +114628,7 @@ var BridgeHandler = class {
114596
114628
  bot: {
114597
114629
  name: this.deps.botConfig?.name ?? "Larkway Agent",
114598
114630
  description: this.deps.botConfig?.description ?? "Local agent served through Larkway.",
114599
- gitlab_token_env: this.deps.botConfig?.gitlab_token_env
114631
+ gitlab_token_env: this.deps.botConfig?.git_token_env ?? this.deps.botConfig?.gitlab_token_env
114600
114632
  },
114601
114633
  agentMemory: this.deps.agentMemory,
114602
114634
  repos: [
@@ -114762,7 +114794,8 @@ var BridgeHandler = class {
114762
114794
  botName: this.deps.botConfig?.name,
114763
114795
  backend: this.deps.botConfig?.backend,
114764
114796
  agentMemory: this.deps.agentMemory,
114765
- larkCliProfile: this.deps.larkCliProfile
114797
+ larkCliProfile: this.deps.larkCliProfile,
114798
+ runtimeWarnings: this.runtimeWarnings()
114766
114799
  });
114767
114800
  const backend = this.deps.botConfig?.backend ?? "claude";
114768
114801
  const permissionMode = this.deps.permissionMode ?? (isAgentWorkspace ? "acceptEdits" : "bypassPermissions");
@@ -117872,6 +117905,14 @@ var GitIdentitySchema = external_exports.object({
117872
117905
  name: external_exports.string().min(1),
117873
117906
  email: external_exports.string().email()
117874
117907
  });
117908
+ var GitCloneUrlSchema = external_exports.string().min(1).refine(
117909
+ (value) => {
117910
+ if (/^https?:\/\/\S+$/i.test(value)) return true;
117911
+ if (/^ssh:\/\/\S+$/i.test(value)) return true;
117912
+ return /^[A-Za-z0-9_.-]+@[^:\s]+:[^ \t\r\n]+$/.test(value);
117913
+ },
117914
+ "url must be an http(s), ssh://, or scp-like Git clone URL"
117915
+ );
117875
117916
  var BotConfigSchema = external_exports.object({
117876
117917
  /** Unique identifier, kebab-case. Used as key in sessionStore. */
117877
117918
  id: external_exports.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "id must be kebab-case"),
@@ -117946,7 +117987,7 @@ var BotConfigSchema = external_exports.object({
117946
117987
  * Agent workspace runtime: URL is a pointer only; the Agent decides clone
117947
117988
  * timing and destination.
117948
117989
  */
117949
- url: external_exports.string().url().optional()
117990
+ url: GitCloneUrlSchema.optional()
117950
117991
  })
117951
117992
  ).default([]),
117952
117993
  /**
@@ -117977,13 +118018,23 @@ var BotConfigSchema = external_exports.object({
117977
118018
  */
117978
118019
  lark_cli_profile: external_exports.string().min(1).optional(),
117979
118020
  /**
117980
- * Env-var *name* (not value) that holds this bot's GitLab PAT.
118021
+ * Env-var *name* (not value) that holds this bot's Git PAT.
117981
118022
  * Read from process.env at startup and injected as GITLAB_TOKEN into the
117982
- * claude subprocess env, so MRs/git ops use the bot's own GitLab account.
117983
- * When absent, the claude subprocess inherits process.env.GITLAB_TOKEN
118023
+ * agent subprocess env, so MRs/git ops use the bot's own account.
118024
+ * When absent, the subprocess inherits process.env.GITLAB_TOKEN
117984
118025
  * (V1 single-bot behavior — one global token).
118026
+ *
118027
+ * Preferred field name; `gitlab_token_env` below is the backward-compat alias.
118028
+ */
118029
+ git_token_env: external_exports.string().min(1).optional(),
118030
+ // preferred: generic git PAT env-var name
118031
+ /**
118032
+ * @deprecated Backward-compat alias for `git_token_env`. If both are present,
118033
+ * main.ts prefers `git_token_env`. Old bot yamls with this field continue to
118034
+ * work without changes.
117985
118035
  */
117986
118036
  gitlab_token_env: external_exports.string().min(1).optional(),
118037
+ // compat alias (legacy)
117987
118038
  /**
117988
118039
  * L2 Agent Memory (职能定义) — filename relative to the bots/ directory,
117989
118040
  * pointing at this bot's `*.memory.md`. Loaded at startup and injected into
@@ -118294,12 +118345,12 @@ var SIGKILL_GRACE_MS = 5e3;
118294
118345
  function buildEnv(botGitIdentity, gitlabToken) {
118295
118346
  const env = { ...process.env };
118296
118347
  delete env["ANTHROPIC_API_KEY"];
118297
- const name = botGitIdentity?.name ?? "larkway-bot";
118298
- const email = botGitIdentity?.email ?? "bot@larkway.local";
118299
- env["GIT_AUTHOR_NAME"] = name;
118300
- env["GIT_AUTHOR_EMAIL"] = email;
118301
- env["GIT_COMMITTER_NAME"] = name;
118302
- env["GIT_COMMITTER_EMAIL"] = email;
118348
+ if (botGitIdentity) {
118349
+ env["GIT_AUTHOR_NAME"] = botGitIdentity.name;
118350
+ env["GIT_AUTHOR_EMAIL"] = botGitIdentity.email;
118351
+ env["GIT_COMMITTER_NAME"] = botGitIdentity.name;
118352
+ env["GIT_COMMITTER_EMAIL"] = botGitIdentity.email;
118353
+ }
118303
118354
  if (gitlabToken !== void 0) {
118304
118355
  env["GITLAB_TOKEN"] = gitlabToken;
118305
118356
  }
@@ -118608,12 +118659,12 @@ function buildCodexEnv(botGitIdentity, gitlabToken) {
118608
118659
  const env = { ...process.env };
118609
118660
  delete env["OPENAI_API_KEY"];
118610
118661
  delete env["ANTHROPIC_API_KEY"];
118611
- const name = botGitIdentity?.name ?? "larkway-bot";
118612
- const email = botGitIdentity?.email ?? "bot@larkway.local";
118613
- env["GIT_AUTHOR_NAME"] = name;
118614
- env["GIT_AUTHOR_EMAIL"] = email;
118615
- env["GIT_COMMITTER_NAME"] = name;
118616
- env["GIT_COMMITTER_EMAIL"] = email;
118662
+ if (botGitIdentity) {
118663
+ env["GIT_AUTHOR_NAME"] = botGitIdentity.name;
118664
+ env["GIT_AUTHOR_EMAIL"] = botGitIdentity.email;
118665
+ env["GIT_COMMITTER_NAME"] = botGitIdentity.name;
118666
+ env["GIT_COMMITTER_EMAIL"] = botGitIdentity.email;
118667
+ }
118617
118668
  if (gitlabToken !== void 0) {
118618
118669
  env["GITLAB_TOKEN"] = gitlabToken;
118619
118670
  }
@@ -118999,33 +119050,144 @@ function checkGrantMatchesBot(normalizedGrantText, bot) {
118999
119050
  return { ok: true };
119000
119051
  }
119001
119052
 
119002
- // src/main.ts
119003
- var STATUS_WRITE_INTERVAL_MS = 3e4;
119004
- var VERSION = resolveLarkwayVersion(import.meta.url);
119005
- function checkCli(name) {
119053
+ // src/runtimeRequirements.ts
119054
+ import { execFileSync } from "node:child_process";
119055
+ function checkCli(command) {
119056
+ try {
119057
+ execFileSync("which", [command], { stdio: ["pipe", "pipe", "pipe"] });
119058
+ } catch {
119059
+ return { ok: false };
119060
+ }
119006
119061
  try {
119007
- const p = execSync(`which ${name}`, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
119008
- if (!p) return { ok: false, version: "" };
119009
- const version = execSync(`${name} --version`, {
119010
- stdio: ["pipe", "pipe", "pipe"]
119011
- }).toString().trim().split("\n")[0] ?? "";
119012
- return { ok: true, version };
119062
+ const version = execFileSync(command, ["--version"], { stdio: ["pipe", "pipe", "pipe"] }).toString().trim().split("\n")[0];
119063
+ return { ok: true, ...version ? { version } : {} };
119013
119064
  } catch {
119014
- return { ok: false, version: "" };
119065
+ return { ok: true };
119015
119066
  }
119016
119067
  }
119017
- function externalClisForBots(bots) {
119018
- const backendBins = bots.map((bot) => bot.backend ?? "claude");
119019
- return Array.from(/* @__PURE__ */ new Set(["lark-cli", ...backendBins, "glab"]));
119068
+ function repoLooksGitLab(urlOrSlug) {
119069
+ return /(^|[./:@-])gitlab([./:@-]|$)/i.test(urlOrSlug);
119020
119070
  }
119071
+ function botUsesGitLab(bot) {
119072
+ return bot.repos.some((repo) => repoLooksGitLab(repo.url ?? repo.slug));
119073
+ }
119074
+ function addCliRequirement(requirements, input) {
119075
+ const existing = requirements.get(`cli:${input.command}`);
119076
+ if (existing) {
119077
+ existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
119078
+ if (existing.severity === "optional" && input.severity === "required") {
119079
+ existing.severity = "required";
119080
+ }
119081
+ return;
119082
+ }
119083
+ const probe = checkCli(input.command);
119084
+ requirements.set(`cli:${input.command}`, {
119085
+ id: `cli:${input.command}`,
119086
+ label: input.label ?? input.command,
119087
+ command: input.command,
119088
+ kind: "cli",
119089
+ severity: input.severity,
119090
+ ok: probe.ok,
119091
+ ...probe.version ? { version: probe.version } : {},
119092
+ reason: input.reason,
119093
+ ...input.installHint ? { installHint: input.installHint } : {},
119094
+ botIds: Array.from(new Set(input.botIds)).sort()
119095
+ });
119096
+ }
119097
+ function addSecretRequirement(requirements, input) {
119098
+ const id = `secret:${input.envName}`;
119099
+ const existing = requirements.get(id);
119100
+ if (existing) {
119101
+ existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
119102
+ return;
119103
+ }
119104
+ const value = process.env[input.envName];
119105
+ requirements.set(id, {
119106
+ id,
119107
+ label: input.envName,
119108
+ kind: "secret",
119109
+ severity: input.severity ?? "required",
119110
+ ok: value != null && value !== "",
119111
+ reason: input.reason,
119112
+ installHint: `Set ${input.envName} in ~/.larkway/.env and restart Larkway.`,
119113
+ botIds: Array.from(new Set(input.botIds)).sort()
119114
+ });
119115
+ }
119116
+ function runtimeRequirementsForBots(bots) {
119117
+ const requirements = /* @__PURE__ */ new Map();
119118
+ if (bots.length > 0) {
119119
+ addCliRequirement(requirements, {
119120
+ command: "lark-cli",
119121
+ label: "Feishu CLI",
119122
+ severity: "required",
119123
+ botIds: bots.map((bot) => bot.id),
119124
+ reason: "Required for agents to read Feishu topic history, attachments, docs, and other context.",
119125
+ installHint: "Install and configure lark-cli, then restart Larkway."
119126
+ });
119127
+ }
119128
+ for (const bot of bots) {
119129
+ addCliRequirement(requirements, {
119130
+ command: bot.backend ?? "claude",
119131
+ severity: "required",
119132
+ botIds: [bot.id],
119133
+ reason: `Required to run this bot's ${bot.backend ?? "claude"} backend.`
119134
+ });
119135
+ if (bot.runtime === "agent_workspace" && bot.repos.length > 0) {
119136
+ const tokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
119137
+ if (tokenEnvName) {
119138
+ addSecretRequirement(requirements, {
119139
+ envName: tokenEnvName,
119140
+ botIds: [bot.id],
119141
+ severity: "optional",
119142
+ reason: "Optional Git identity material. When absent, agents use the host's normal Git auth."
119143
+ });
119144
+ } else {
119145
+ requirements.set(`secret:missing-git-token:${bot.id}`, {
119146
+ id: `secret:missing-git-token:${bot.id}`,
119147
+ label: "Git access token env",
119148
+ kind: "secret",
119149
+ severity: "optional",
119150
+ ok: false,
119151
+ reason: `Bot "${bot.id}" has repo pointers but no git_token_env. It will use the host's normal Git auth.`,
119152
+ installHint: "Add a Git access token only when you want this agent to use a specific GitHub/GitLab identity.",
119153
+ botIds: [bot.id]
119154
+ });
119155
+ }
119156
+ }
119157
+ if (botUsesGitLab(bot)) {
119158
+ addCliRequirement(requirements, {
119159
+ command: "glab",
119160
+ label: "GitLab CLI",
119161
+ severity: "optional",
119162
+ botIds: [bot.id],
119163
+ reason: "Only needed when an agent uses GitLab-specific actions such as MRs, pipelines, or releases.",
119164
+ installHint: "Install glab only for bots that need GitLab-specific workflow commands."
119165
+ });
119166
+ }
119167
+ }
119168
+ return [...requirements.values()].sort((a, b) => {
119169
+ if (a.severity !== b.severity) return a.severity === "required" ? -1 : 1;
119170
+ if (a.kind !== b.kind) return a.kind === "cli" ? -1 : 1;
119171
+ return a.label.localeCompare(b.label);
119172
+ });
119173
+ }
119174
+
119175
+ // src/main.ts
119176
+ var STATUS_WRITE_INTERVAL_MS = 3e4;
119177
+ var VERSION = resolveLarkwayVersion(import.meta.url);
119021
119178
  function printExternalCliProbe(bots) {
119022
- console.log("External CLIs:");
119023
- for (const name of externalClisForBots(bots)) {
119024
- const { ok, version } = checkCli(name);
119025
- if (ok) {
119026
- console.log(` ${name.padEnd(10)} \u2713 ${version}`);
119179
+ console.log("Runtime requirements:");
119180
+ for (const req of runtimeRequirementsForBots(bots)) {
119181
+ const icon = req.ok ? "\u2713" : "\u2717";
119182
+ const tag = req.severity === "optional" ? "optional" : "required";
119183
+ const target = req.command ?? req.label;
119184
+ const botScope = req.botIds.length > 0 ? ` [${req.botIds.join(", ")}]` : "";
119185
+ if (req.ok) {
119186
+ console.log(` ${target.padEnd(14)} ${icon} ${req.version ?? tag}${botScope}`);
119027
119187
  } else {
119028
- console.warn(` ${name.padEnd(10)} \u2717 (not found \u2014 install before running bots that use it)`);
119188
+ const message = req.kind === "secret" ? req.reason : `not found \u2014 ${req.reason}`;
119189
+ console.warn(` ${target.padEnd(14)} ${icon} (${tag}) ${message}${botScope}`);
119190
+ if (req.installHint) console.warn(` ${"".padEnd(14)} ${req.installHint}`);
119029
119191
  }
119030
119192
  }
119031
119193
  }
@@ -119057,19 +119219,18 @@ async function runV2Mode({
119057
119219
  continue;
119058
119220
  }
119059
119221
  if (bot.runtime === "agent_workspace") {
119060
- if (bot.repos.length > 0 && !bot.gitlab_token_env) {
119061
- console.error(
119062
- `[larkway] SKIPPING bot "${bot.id}": runtime=agent_workspace has repo pointers but no gitlab_token_env. v0.3 workspace repo bots must reference an explicit per-agent token env name; they do not inherit global GITLAB_TOKEN.`
119222
+ const _agentTokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
119223
+ if (bot.repos.length > 0 && !_agentTokenEnvName) {
119224
+ console.warn(
119225
+ `[larkway] bot "${bot.id}": runtime=agent_workspace has repo pointers but no git_token_env. Starting anyway; the agent will use the host's normal Git identity/auth.`
119063
119226
  );
119064
- continue;
119065
119227
  }
119066
- if (bot.gitlab_token_env) {
119067
- const gitlabToken = process.env[bot.gitlab_token_env];
119068
- if (gitlabToken == null || gitlabToken === "") {
119069
- console.error(
119070
- `[larkway] SKIPPING bot "${bot.id}": runtime=agent_workspace declares gitlab_token_env="${bot.gitlab_token_env}", but that env var is unset/empty. Set the real token in ~/.larkway/.env; v0.3 workspace bots do not fall back to global GITLAB_TOKEN.`
119228
+ if (_agentTokenEnvName) {
119229
+ const agentToken = process.env[_agentTokenEnvName];
119230
+ if (agentToken == null || agentToken === "") {
119231
+ console.warn(
119232
+ `[larkway] bot "${bot.id}": runtime=agent_workspace declares token env "${_agentTokenEnvName}", but that env var is unset/empty. Starting anyway; the agent will use the host's normal Git identity/auth.`
119071
119233
  );
119072
- continue;
119073
119234
  }
119074
119235
  }
119075
119236
  const permissionGate = await checkWorkspacePermissionGrant(resolveAgentWorkspacePath(bot.id), bot);
@@ -119096,10 +119257,11 @@ async function runV2Mode({
119096
119257
  const instances = [];
119097
119258
  for (const bot of healthyBots) {
119098
119259
  const appSecret = process.env[bot.app_secret_env];
119099
- const gitlabToken = bot.gitlab_token_env != null ? process.env[bot.gitlab_token_env] : void 0;
119100
- if (bot.gitlab_token_env != null && (gitlabToken == null || gitlabToken === "")) {
119260
+ const tokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
119261
+ const gitlabToken = tokenEnvName != null ? process.env[tokenEnvName] : void 0;
119262
+ if (tokenEnvName != null && (gitlabToken == null || gitlabToken === "")) {
119101
119263
  console.warn(
119102
- `[larkway] bot "${bot.id}" declares gitlab_token_env="${bot.gitlab_token_env}" but that env var is unset/empty \u2014 falling back to global GITLAB_TOKEN.`
119264
+ `[larkway] bot "${bot.id}" declares token env "${tokenEnvName}" but that env var is unset/empty \u2014 leaving the host Git auth environment unchanged.`
119103
119265
  );
119104
119266
  }
119105
119267
  const larkCliProfile = deriveLarkCliProfile(bot.lark_cli_profile, bot.app_id);
@@ -119168,9 +119330,9 @@ async function runV2Mode({
119168
119330
  portRangeStart: configJson.conventions.portRangeStart,
119169
119331
  portRangeEnd: configJson.conventions.portRangeEnd,
119170
119332
  readOnly: bot.read_only,
119171
- gitlabTokenEnvName: bot.gitlab_token_env
119333
+ gitlabTokenEnvName: tokenEnvName
119172
119334
  };
119173
- const effectiveGitlabToken = bot.runtime === "agent_workspace" ? gitlabToken ?? "" : gitlabToken;
119335
+ const effectiveGitlabToken = gitlabToken;
119174
119336
  const resolvedPeers = bot.peers.flatMap((peerId) => {
119175
119337
  const peer = bots.find((b) => b.id === peerId);
119176
119338
  if (!peer) return [];
@@ -119191,11 +119353,13 @@ async function runV2Mode({
119191
119353
  git_identity: bot.git_identity,
119192
119354
  backend: bot.backend,
119193
119355
  runtime: bot.runtime,
119356
+ git_token_env: bot.git_token_env,
119194
119357
  gitlab_token_env: bot.gitlab_token_env
119195
119358
  },
119196
119359
  gitlabToken: effectiveGitlabToken,
119197
119360
  agentMemory: bot.agent_memory,
119198
119361
  larkCliProfile,
119362
+ runtimeRequirements: runtimeRequirementsForBots([bot]),
119199
119363
  recordRuntimeEvent: async (patch) => {
119200
119364
  await upsertRuntimeEvent(larkwayHome(), bot.id, patch);
119201
119365
  }