larkway 0.3.8 → 0.3.10

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.6**
11
+ **Current release: v0.3.10**
12
12
 
13
13
  ---
14
14
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.6**
11
+ **当前版本:v0.3.10**
12
12
 
13
13
  ---
14
14
 
package/dist/cli/index.js CHANGED
@@ -119646,6 +119646,14 @@ var GitIdentitySchema = external_exports.object({
119646
119646
  name: external_exports.string().min(1),
119647
119647
  email: external_exports.string().email()
119648
119648
  });
119649
+ var GitCloneUrlSchema = external_exports.string().min(1).refine(
119650
+ (value) => {
119651
+ if (/^https?:\/\/\S+$/i.test(value)) return true;
119652
+ if (/^ssh:\/\/\S+$/i.test(value)) return true;
119653
+ return /^[A-Za-z0-9_.-]+@[^:\s]+:[^ \t\r\n]+$/.test(value);
119654
+ },
119655
+ "url must be an http(s), ssh://, or scp-like Git clone URL"
119656
+ );
119649
119657
  var BotConfigSchema = external_exports.object({
119650
119658
  /** Unique identifier, kebab-case. Used as key in sessionStore. */
119651
119659
  id: external_exports.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "id must be kebab-case"),
@@ -119720,7 +119728,7 @@ var BotConfigSchema = external_exports.object({
119720
119728
  * Agent workspace runtime: URL is a pointer only; the Agent decides clone
119721
119729
  * timing and destination.
119722
119730
  */
119723
- url: external_exports.string().url().optional()
119731
+ url: GitCloneUrlSchema.optional()
119724
119732
  })
119725
119733
  ).default([]),
119726
119734
  /**
@@ -120016,7 +120024,7 @@ function renderBotYaml(config) {
120016
120024
  const header = [
120017
120025
  "# Larkway bot config (L1) \u2014 generated/edited via `larkway` CLI.",
120018
120026
  "# Credentials are env-var NAMES, never values:",
120019
- "# app_secret_env / gitlab_token_env reference keys in ~/.larkway/.env (chmod 0600).",
120027
+ "# app_secret_env / git_token_env reference keys in ~/.larkway/.env (chmod 0600).",
120020
120028
  "# L2 \u804C\u80FD memory lives alongside in <id>.memory.md (memory_file points at it).",
120021
120029
  ""
120022
120030
  ].join("\n");
@@ -122196,14 +122204,14 @@ async function runAdd(ctx, args) {
122196
122204
  });
122197
122205
  if (repoSlug) {
122198
122206
  const repoBranch = await ui.prompt("\u9ED8\u8BA4\u5206\u652F:", {
122199
- default: setMap.get("repo_branch") ?? "master",
122207
+ default: setMap.get("repo_branch") ?? "main",
122200
122208
  nonInteractive: flags.nonInteractive
122201
122209
  });
122202
- const repoUrl = await ui.prompt("clone URL (\u53EF\u9009,\u7559\u7A7A=\u7528\u9ED8\u8BA4 remote \u63A8\u5BFC):", {
122210
+ const repoUrl = await ui.prompt("clone URL (\u5EFA\u8BAE\u586B\u5199;agent \u7528\u5B83 clone):", {
122203
122211
  default: setMap.get("repo_url") ?? "",
122204
122212
  nonInteractive: flags.nonInteractive
122205
122213
  });
122206
- const repoEntry = { slug: repoSlug, branch: repoBranch || "master" };
122214
+ const repoEntry = { slug: repoSlug, branch: repoBranch || "main" };
122207
122215
  if (repoUrl) repoEntry.url = repoUrl;
122208
122216
  repos.push(repoEntry);
122209
122217
  }
@@ -122212,7 +122220,7 @@ async function runAdd(ctx, args) {
122212
122220
  if (repoSlug) {
122213
122221
  const repoEntry = {
122214
122222
  slug: repoSlug,
122215
- branch: setMap.get("repo_branch") ?? "master"
122223
+ branch: setMap.get("repo_branch") ?? "main"
122216
122224
  };
122217
122225
  const repoUrl = setMap.get("repo_url");
122218
122226
  if (repoUrl) repoEntry.url = repoUrl;
@@ -122220,12 +122228,11 @@ async function runAdd(ctx, args) {
122220
122228
  }
122221
122229
  }
122222
122230
  let gitlab_token_env;
122223
- const defaultGitlabTokenEnv2 = repos.length > 0 ? `LARKWAY_${id.toUpperCase().replace(/-/g, "_")}_GITLAB_TOKEN` : "";
122224
- if (flags.advanced || setMap.has("gitlab_token_env") || repos.length > 0) {
122231
+ if (flags.advanced || setMap.has("gitlab_token_env")) {
122225
122232
  const rawGitlab = await ui.prompt(
122226
- "Git access token \u73AF\u5883\u53D8\u91CF\u540D(\u4EC5\u8BB0\u5F55 KEY,\u771F\u503C\u5199 ~/.larkway/.env):",
122233
+ "Git access token \u73AF\u5883\u53D8\u91CF\u540D(\u53EF\u9009;\u79C1\u6709\u5E93/push/MR \u624D\u9700\u8981):",
122227
122234
  {
122228
- default: setMap.get("gitlab_token_env") ?? defaultGitlabTokenEnv2,
122235
+ default: setMap.get("gitlab_token_env") ?? "",
122229
122236
  nonInteractive: flags.nonInteractive
122230
122237
  }
122231
122238
  );
@@ -122236,10 +122243,6 @@ async function runAdd(ctx, args) {
122236
122243
  return 1;
122237
122244
  }
122238
122245
  }
122239
- if (repos.length > 0 && !gitlab_token_env) {
122240
- ui.failure("agent_workspace repo bot \u5FC5\u987B\u58F0\u660E git_token_env(\u6216 gitlab_token_env);v0.3 \u4E0D\u7EE7\u627F\u5168\u5C40 GITLAB_TOKEN\u3002");
122241
- return 1;
122242
- }
122243
122246
  let lark_cli_profile;
122244
122247
  if (flags.advanced || setMap.has("lark_cli_profile")) {
122245
122248
  const rawProfile = await ui.prompt(
@@ -124003,24 +124006,16 @@ import { spawn as spawn4 } from "node:child_process";
124003
124006
  import { readFile as readFile10, access as access9 } from "node:fs/promises";
124004
124007
  import "node:module";
124005
124008
  import path17 from "node:path";
124006
- var DEFAULT_GITLAB_HOST = "github.com";
124007
- var DEFAULT_RELEASE_REPO = "chuckwu0/larkway";
124008
- var LATEST_TGZ_NAME = "larkway-latest.tgz";
124009
- function buildLatestUrl(gitlabHost, repo) {
124010
- return `https://${gitlabHost}/git/${repo}/-/releases/permalink/latest/downloads/${LATEST_TGZ_NAME}`;
124009
+ var DEFAULT_NPM_PACKAGE_SPEC = "larkway@latest";
124010
+ function resolveNpmPackageSpec() {
124011
+ return process.env["LARKWAY_UPDATE_URL"] ?? DEFAULT_NPM_PACKAGE_SPEC;
124011
124012
  }
124012
- function resolveReleaseCoords() {
124013
- const gitlabHost = process.env["GITLAB_HOST"] ?? DEFAULT_GITLAB_HOST;
124014
- const repo = process.env["LARKWAY_RELEASE_REPO"] ?? DEFAULT_RELEASE_REPO;
124015
- const latestUrl = process.env["LARKWAY_UPDATE_URL"] ?? buildLatestUrl(gitlabHost, repo);
124016
- return { gitlabHost, repo, latestUrl };
124017
- }
124018
- function buildReleaseSteps(latestUrl, cwd) {
124013
+ function buildNpmSteps(packageSpec, cwd) {
124019
124014
  return [
124020
124015
  {
124021
- label: `npm i -g ${latestUrl}`,
124016
+ label: `npm i -g ${packageSpec}`,
124022
124017
  cmd: "npm",
124023
- args: ["i", "-g", latestUrl],
124018
+ args: ["i", "-g", packageSpec],
124024
124019
  cwd
124025
124020
  },
124026
124021
  {
@@ -124174,24 +124169,24 @@ async function run7(ctx, args) {
124174
124169
  const { ui, flags, cwd } = ctx;
124175
124170
  const isDryRun = args.includes("--dry-run");
124176
124171
  const forceGitPull = args.includes("--git-pull");
124177
- const { latestUrl } = resolveReleaseCoords();
124172
+ const packageSpec = resolveNpmPackageSpec();
124178
124173
  if (!forceGitPull) {
124179
- const releaseSteps = buildReleaseSteps(latestUrl, cwd);
124174
+ const npmSteps = buildNpmSteps(packageSpec, cwd);
124180
124175
  const lifecycleLabels2 = /* @__PURE__ */ new Set(["larkway stop (bridge lifecycle)", "larkway start (bridge lifecycle)"]);
124181
124176
  if (isDryRun) {
124182
124177
  if (flags.json) {
124183
124178
  ui.emitJson({
124184
124179
  ok: true,
124185
124180
  dryRun: true,
124186
- mode: "release",
124187
- latestUrl,
124188
- steps: releaseSteps.map((s) => ({ label: s.label, cmd: s.cmd, args: s.args, cwd: s.cwd }))
124181
+ mode: "npm",
124182
+ packageSpec,
124183
+ steps: npmSteps.map((s) => ({ label: s.label, cmd: s.cmd, args: s.args, cwd: s.cwd }))
124189
124184
  });
124190
124185
  } else {
124191
- ui.print(ui.bold("larkway update --dry-run (release \u6E90\u6A21\u5F0F)"));
124192
- ui.print(ui.dim(` latest url: ${latestUrl}`));
124186
+ ui.print(ui.bold("larkway update --dry-run (npm \u6E90\u6A21\u5F0F)"));
124187
+ ui.print(ui.dim(` package: ${packageSpec}`));
124193
124188
  ui.print("");
124194
- releaseSteps.forEach((s, i) => {
124189
+ npmSteps.forEach((s, i) => {
124195
124190
  ui.print(` ${ui.cyan(String(i + 1) + ".")} ${s.label}`);
124196
124191
  ui.print(ui.dim(` $ ${s.cmd} ${s.args.join(" ")}`));
124197
124192
  });
@@ -124202,14 +124197,14 @@ async function run7(ctx, args) {
124202
124197
  return 0;
124203
124198
  }
124204
124199
  if (flags.json) {
124205
- ui.emitJson({ ok: true, status: "starting", mode: "release", latestUrl, stepCount: releaseSteps.length });
124200
+ ui.emitJson({ ok: true, status: "starting", mode: "npm", packageSpec, stepCount: npmSteps.length });
124206
124201
  } else {
124207
- ui.print(ui.bold("larkway update (release \u6E90)"));
124208
- ui.print(ui.dim(` latest: ${latestUrl}`));
124202
+ ui.print(ui.bold("larkway update (npm \u6E90)"));
124203
+ ui.print(ui.dim(` package: ${packageSpec}`));
124209
124204
  }
124210
- const { ok: ok3, lifecycleWarnings: lifecycleWarnings2 } = await runSteps(releaseSteps, lifecycleLabels2, flags, ui);
124205
+ const { ok: ok3, lifecycleWarnings: lifecycleWarnings2 } = await runSteps(npmSteps, lifecycleLabels2, flags, ui);
124211
124206
  if (!ok3) {
124212
- const fallbackMsg = "release \u5347\u7EA7\u5931\u8D25\u3002\u82E5\u7F51\u7EDC\u4E0D\u901A\u6216 latest \u94FE\u63A5\u672A\u5C31\u7EEA,\u53EF\u6539\u7528 `larkway update --git-pull` \u8D70 git pull + build \u8DEF\u5F84\u3002";
124207
+ const fallbackMsg = "npm \u5347\u7EA7\u5931\u8D25\u3002\u82E5\u7F51\u7EDC\u4E0D\u901A\u3001npm \u6743\u9650\u4E0D\u8DB3\u6216\u6E90\u4E0D\u53EF\u7528,\u53EF\u6539\u7528 `larkway update --git-pull` \u8D70 git pull + build \u8DEF\u5F84\u3002";
124213
124208
  if (flags.json) {
124214
124209
  ui.emitJson({ ok: false, hint: fallbackMsg });
124215
124210
  } else {
@@ -124223,14 +124218,14 @@ async function run7(ctx, args) {
124223
124218
  if (hadWarning2) {
124224
124219
  ui.emitJson({ ok: true, status: "complete_with_warnings", warning: lifecycleWarnings2.join("; ") });
124225
124220
  } else {
124226
- ui.emitJson({ ok: true, status: "complete", mode: "release" });
124221
+ ui.emitJson({ ok: true, status: "complete", mode: "npm" });
124227
124222
  }
124228
124223
  } else {
124229
124224
  ui.print("");
124230
124225
  if (hadWarning2) {
124231
124226
  ui.success("larkway update \u5B8C\u6210,\u4F46 bridge \u9700\u624B\u52A8\u91CD\u542F(\u8BF7\u8FD0\u884C `larkway start`)\u3002");
124232
124227
  } else {
124233
- ui.success("larkway update \u5B8C\u6210\u3002\u5DF2\u5347\u7EA7\u5230\u6700\u65B0 release,bridge \u5DF2\u91CD\u542F\u3002");
124228
+ ui.success("larkway update \u5B8C\u6210\u3002\u5DF2\u5347\u7EA7\u5230\u6700\u65B0 npm \u7248\u672C,bridge \u5DF2\u91CD\u542F\u3002");
124234
124229
  }
124235
124230
  }
124236
124231
  return 0;
@@ -124556,10 +124551,10 @@ function defaultOnboardPermissionRequests(input) {
124556
124551
  items.push(`Feishu chat allowlist: ${input.chats.join(", ")}`);
124557
124552
  }
124558
124553
  for (const repo of input.repos) {
124559
- items.push(`GitLab repo pointer: ${repo.slug} (${repo.branch})`);
124554
+ items.push(`Git repo pointer: ${repo.slug} (${repo.branch})`);
124560
124555
  }
124561
124556
  if (input.gitlab_token_env) {
124562
- items.push(`GitLab token env name: ${input.gitlab_token_env}`);
124557
+ items.push(`Agent-level Git token env name: ${input.gitlab_token_env}`);
124563
124558
  }
124564
124559
  items.push("Local shell inside the Agent Workspace for task execution and verification");
124565
124560
  return items;
@@ -124706,6 +124701,128 @@ async function cancelOnboard(sessionId) {
124706
124701
  return { cancelled: true };
124707
124702
  }
124708
124703
 
124704
+ // src/runtimeRequirements.ts
124705
+ import { execFileSync } from "node:child_process";
124706
+ function checkCli(command) {
124707
+ try {
124708
+ execFileSync("which", [command], { stdio: ["pipe", "pipe", "pipe"] });
124709
+ } catch {
124710
+ return { ok: false };
124711
+ }
124712
+ try {
124713
+ const version = execFileSync(command, ["--version"], { stdio: ["pipe", "pipe", "pipe"] }).toString().trim().split("\n")[0];
124714
+ return { ok: true, ...version ? { version } : {} };
124715
+ } catch {
124716
+ return { ok: true };
124717
+ }
124718
+ }
124719
+ function repoLooksGitLab(urlOrSlug) {
124720
+ return /(^|[./:@-])gitlab([./:@-]|$)/i.test(urlOrSlug);
124721
+ }
124722
+ function botUsesGitLab(bot) {
124723
+ return bot.repos.some((repo) => repoLooksGitLab(repo.url ?? repo.slug));
124724
+ }
124725
+ function addCliRequirement(requirements, input) {
124726
+ const existing = requirements.get(`cli:${input.command}`);
124727
+ if (existing) {
124728
+ existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
124729
+ if (existing.severity === "optional" && input.severity === "required") {
124730
+ existing.severity = "required";
124731
+ }
124732
+ return;
124733
+ }
124734
+ const probe = checkCli(input.command);
124735
+ requirements.set(`cli:${input.command}`, {
124736
+ id: `cli:${input.command}`,
124737
+ label: input.label ?? input.command,
124738
+ command: input.command,
124739
+ kind: "cli",
124740
+ severity: input.severity,
124741
+ ok: probe.ok,
124742
+ ...probe.version ? { version: probe.version } : {},
124743
+ reason: input.reason,
124744
+ ...input.installHint ? { installHint: input.installHint } : {},
124745
+ botIds: Array.from(new Set(input.botIds)).sort()
124746
+ });
124747
+ }
124748
+ function addSecretRequirement(requirements, input) {
124749
+ const id = `secret:${input.envName}`;
124750
+ const existing = requirements.get(id);
124751
+ if (existing) {
124752
+ existing.botIds = Array.from(/* @__PURE__ */ new Set([...existing.botIds, ...input.botIds])).sort();
124753
+ return;
124754
+ }
124755
+ const value = process.env[input.envName];
124756
+ requirements.set(id, {
124757
+ id,
124758
+ label: input.envName,
124759
+ kind: "secret",
124760
+ severity: input.severity ?? "required",
124761
+ ok: value != null && value !== "",
124762
+ reason: input.reason,
124763
+ installHint: `Set ${input.envName} in ~/.larkway/.env and restart Larkway.`,
124764
+ botIds: Array.from(new Set(input.botIds)).sort()
124765
+ });
124766
+ }
124767
+ function runtimeRequirementsForBots(bots) {
124768
+ const requirements = /* @__PURE__ */ new Map();
124769
+ if (bots.length > 0) {
124770
+ addCliRequirement(requirements, {
124771
+ command: "lark-cli",
124772
+ label: "Feishu CLI",
124773
+ severity: "required",
124774
+ botIds: bots.map((bot) => bot.id),
124775
+ reason: "Required for agents to read Feishu topic history, attachments, docs, and other context.",
124776
+ installHint: "Install and configure lark-cli, then restart Larkway."
124777
+ });
124778
+ }
124779
+ for (const bot of bots) {
124780
+ addCliRequirement(requirements, {
124781
+ command: bot.backend ?? "claude",
124782
+ severity: "required",
124783
+ botIds: [bot.id],
124784
+ reason: `Required to run this bot's ${bot.backend ?? "claude"} backend.`
124785
+ });
124786
+ if (bot.runtime === "agent_workspace" && bot.repos.length > 0) {
124787
+ const tokenEnvName = bot.git_token_env ?? bot.gitlab_token_env;
124788
+ if (tokenEnvName) {
124789
+ addSecretRequirement(requirements, {
124790
+ envName: tokenEnvName,
124791
+ botIds: [bot.id],
124792
+ severity: "optional",
124793
+ reason: "Optional Git identity material. When absent, agents use the host's normal Git auth."
124794
+ });
124795
+ } else {
124796
+ requirements.set(`secret:missing-git-token:${bot.id}`, {
124797
+ id: `secret:missing-git-token:${bot.id}`,
124798
+ label: "Git access token env",
124799
+ kind: "secret",
124800
+ severity: "optional",
124801
+ ok: false,
124802
+ reason: `Bot "${bot.id}" has repo pointers but no git_token_env. It will use the host's normal Git auth.`,
124803
+ installHint: "Add a Git access token only when you want this agent to use a specific GitHub/GitLab identity.",
124804
+ botIds: [bot.id]
124805
+ });
124806
+ }
124807
+ }
124808
+ if (botUsesGitLab(bot)) {
124809
+ addCliRequirement(requirements, {
124810
+ command: "glab",
124811
+ label: "GitLab CLI",
124812
+ severity: "optional",
124813
+ botIds: [bot.id],
124814
+ reason: "Only needed when an agent uses GitLab-specific actions such as MRs, pipelines, or releases.",
124815
+ installHint: "Install glab only for bots that need GitLab-specific workflow commands."
124816
+ });
124817
+ }
124818
+ }
124819
+ return [...requirements.values()].sort((a, b) => {
124820
+ if (a.severity !== b.severity) return a.severity === "required" ? -1 : 1;
124821
+ if (a.kind !== b.kind) return a.kind === "cli" ? -1 : 1;
124822
+ return a.label.localeCompare(b.label);
124823
+ });
124824
+ }
124825
+
124709
124826
  // src/web/api.ts
124710
124827
  var execFileAsync7 = promisify8(execFileCallback2);
124711
124828
  var CHAT_NAME_CACHE_MS = 5 * 60 * 1e3;
@@ -125395,6 +125512,22 @@ var getBridgeLogs = async (req) => {
125395
125512
  json: result
125396
125513
  };
125397
125514
  };
125515
+ var getRuntimeRequirements = async (req) => {
125516
+ const { ctx } = req;
125517
+ const ids = await ctx.stores.botsStore.listBots();
125518
+ const bots = await Promise.all(
125519
+ ids.map(async (id) => ctx.stores.botsStore.readBot(id))
125520
+ );
125521
+ const requirements = runtimeRequirementsForBots(bots);
125522
+ return {
125523
+ status: 200,
125524
+ json: {
125525
+ requirements,
125526
+ missingRequired: requirements.filter((req2) => req2.severity === "required" && !req2.ok),
125527
+ missingOptional: requirements.filter((req2) => req2.severity === "optional" && !req2.ok)
125528
+ }
125529
+ };
125530
+ };
125398
125531
  var BACKEND_META = {
125399
125532
  claude: { id: "claude", name: "Claude Code", short: "Claude", vendor: "Anthropic \u8BA2\u9605", mono: "CC" },
125400
125533
  codex: { id: "codex", name: "Codex", short: "Codex", vendor: "OpenAI \u8BA2\u9605", mono: "CX" }
@@ -125436,6 +125569,7 @@ var ROUTES = {
125436
125569
  "GET /api/bridge": getBridge,
125437
125570
  "POST /api/bridge/restart": postBridgeRestart,
125438
125571
  "GET /api/bridge/logs": getBridgeLogs,
125572
+ "GET /api/runtime/requirements": getRuntimeRequirements,
125439
125573
  "GET /api/backends": getBackends
125440
125574
  };
125441
125575
  function matchRoute(method, pathname, routes = ROUTES) {