@yachiyo-5i/xlyra-agent 1.2.0 → 1.3.0

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
@@ -215,7 +215,7 @@ Runtime data is stored under `~/.xlyra-agent/`:
215
215
  | `POST` | `/sessions` | Start a session or continue an existing session |
216
216
  | `GET` | `/sessions` | List recent sessions with pagination |
217
217
  | `GET` | `/sessions/:id/transcript` | Read the complete transcript and compaction records |
218
- | `PATCH` | `/sessions/:id` | Rename a session |
218
+ | `PATCH` | `/sessions/:id` | Rename, or archive/restore (`{archived: true\|false}`; archived = read-only until restored) |
219
219
  | `DELETE` | `/sessions/:id` | Delete an idle session |
220
220
  | `POST` | `/sessions/:id/retry` | Retry from a selected user message |
221
221
  | `POST` | `/sessions/:id/compact-context` | Compact the current model context manually |
@@ -74,11 +74,14 @@ var StaticCredentialProvider = class {
74
74
  };
75
75
  var DEFAULT_RENEW_THRESHOLD_MS = 5 * 6e4;
76
76
  var FALLBACK_TOKEN_TTL_MS = 15 * 6e4;
77
+ var INITIAL_ISSUE_ATTEMPTS = 6;
78
+ var INITIAL_ISSUE_RETRY_DELAY_MS = 500;
77
79
  var XlyraCallbackCredentialProvider = class {
78
80
  constructor(url, opts) {
79
81
  this.url = url;
80
82
  this.fetchImpl = opts?.fetchImpl ?? fetch;
81
83
  this.renewThresholdMs = opts?.renewThresholdMs ?? DEFAULT_RENEW_THRESHOLD_MS;
84
+ this.sleep = opts?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
82
85
  this.now = opts?.now ?? Date.now;
83
86
  }
84
87
  url;
@@ -89,6 +92,7 @@ var XlyraCallbackCredentialProvider = class {
89
92
  inflight = /* @__PURE__ */ new Map();
90
93
  fetchImpl;
91
94
  renewThresholdMs;
95
+ sleep;
92
96
  now;
93
97
  /** 更新当前 run 上下文。token 按 runId 隔离,切换 run 不影响其他 run 的 token */
94
98
  setRunContext(context) {
@@ -161,23 +165,28 @@ var XlyraCallbackCredentialProvider = class {
161
165
  return this.context;
162
166
  }
163
167
  async doIssueInitial(context) {
164
- let res;
165
- try {
166
- res = await this.fetchImpl(this.url, {
167
- method: "POST",
168
- headers: { "content-type": "application/json" },
169
- body: JSON.stringify({
170
- agent_instance_id: context.agentInstanceId,
171
- session_id: context.sessionId,
172
- run_id: context.runId,
173
- model: context.model,
174
- reason: "initial"
175
- })
176
- });
177
- } catch {
178
- return null;
168
+ for (let attempt = 0; attempt < INITIAL_ISSUE_ATTEMPTS; attempt += 1) {
169
+ if (attempt > 0) await this.sleep(INITIAL_ISSUE_RETRY_DELAY_MS);
170
+ let res;
171
+ try {
172
+ res = await this.fetchImpl(this.url, {
173
+ method: "POST",
174
+ headers: { "content-type": "application/json" },
175
+ body: JSON.stringify({
176
+ agent_instance_id: context.agentInstanceId,
177
+ session_id: context.sessionId,
178
+ run_id: context.runId,
179
+ model: context.model,
180
+ reason: "initial"
181
+ })
182
+ });
183
+ } catch {
184
+ continue;
185
+ }
186
+ const cached = await this.storeFromResponse(context.runId, res);
187
+ if (cached) return cached;
179
188
  }
180
- return this.storeFromResponse(context.runId, res);
189
+ return null;
181
190
  }
182
191
  async doRenew(context, cached) {
183
192
  let res;
@@ -3442,6 +3451,8 @@ var appConfigSchema = z13.object({
3442
3451
  enable_agents_md: z13.boolean().optional(),
3443
3452
  /** 是否发现并暴露 Agent Skills(默认 true) */
3444
3453
  enable_skills: z13.boolean().optional(),
3454
+ /** 停用的 skill 名称名单(不删文件,run 时跳过注入;在 enable_skills 之后生效) */
3455
+ disabled_skills: z13.array(z13.string()).optional(),
3445
3456
  /** 额外 skills 根目录(不替换标准路径) */
3446
3457
  skills_dirs: z13.array(z13.string()).optional()
3447
3458
  }).optional()
@@ -3978,7 +3989,11 @@ function loadWorkspaceContext(workdir, opts = {}) {
3978
3989
  const fragment = formatAgentsMdContext(agents);
3979
3990
  if (fragment) fragments.push(fragment);
3980
3991
  }
3981
- const skills = enableSkills ? discoverSkills(workdir, opts.skills) : { skills: [], warnings: [] };
3992
+ let skills = enableSkills ? discoverSkills(workdir, opts.skills) : { skills: [], warnings: [] };
3993
+ const disabled = new Set(opts.disabledSkills ?? []);
3994
+ if (disabled.size > 0 && skills.skills.length > 0) {
3995
+ skills = { ...skills, skills: skills.skills.filter((skill) => !disabled.has(skill.name)) };
3996
+ }
3982
3997
  warnings.push(...skills.warnings);
3983
3998
  const catalog = formatSkillsCatalog(skills);
3984
3999
  if (catalog) fragments.push(catalog);
@@ -3987,6 +4002,8 @@ function loadWorkspaceContext(workdir, opts = {}) {
3987
4002
 
3988
4003
  // src/server/routes.ts
3989
4004
  import { Hono } from "hono";
4005
+ import fs15 from "fs";
4006
+ import path16 from "path";
3990
4007
  import { z as z14 } from "zod";
3991
4008
  function ok(data, message = "") {
3992
4009
  return { success: true, code: 0, message, data };
@@ -4043,8 +4060,9 @@ function createAgentRoutes(ctx) {
4043
4060
  await next();
4044
4061
  });
4045
4062
  }
4063
+ const currentWorkspaceOptions = () => ctx.resolveWorkspaceContext?.() ?? ctx.workspaceContext ?? {};
4046
4064
  const refreshWorkspace = () => {
4047
- const workspace = loadWorkspaceContext(ctx.workdir, ctx.workspaceContext ?? {});
4065
+ const workspace = loadWorkspaceContext(ctx.workdir, currentWorkspaceOptions());
4048
4066
  for (const warning of workspace.warnings) {
4049
4067
  console.warn(`[workspace-context] ${warning}`);
4050
4068
  }
@@ -4403,13 +4421,84 @@ data: ${JSON.stringify(payload)}
4403
4421
  await ctx.registry.cancelSession(c.req.param("id"));
4404
4422
  return c.json(ok({}, "\u5DF2\u8BF7\u6C42\u505C\u6B62\u4F1A\u8BDD"));
4405
4423
  });
4424
+ app.get("/skills", (c) => {
4425
+ const opts = currentWorkspaceOptions();
4426
+ const disabled = new Set(opts.disabledSkills ?? []);
4427
+ const discovered = discoverSkills(ctx.workdir, opts.skills ?? {});
4428
+ return c.json(ok({
4429
+ enabled: opts.enableSkills !== false,
4430
+ skills: discovered.skills.map((skill) => ({
4431
+ name: skill.name,
4432
+ description: skill.description,
4433
+ scope: skill.scope,
4434
+ enabled: !disabled.has(skill.name),
4435
+ path: skill.skillFile,
4436
+ ...skill.license ? { license: skill.license } : {}
4437
+ })),
4438
+ warnings: discovered.warnings
4439
+ }));
4440
+ });
4441
+ const SKILL_FILE_VIEW_MAX_BYTES = 256e3;
4442
+ const findSkill = (name) => {
4443
+ const opts = currentWorkspaceOptions();
4444
+ const discovered = discoverSkills(ctx.workdir, opts.skills ?? {});
4445
+ const skill = discovered.skills.find((item) => item.name === name);
4446
+ return skill ? { skill, disabled: new Set(opts.disabledSkills ?? []) } : null;
4447
+ };
4448
+ app.get("/skills/:name", (c) => {
4449
+ const found = findSkill(c.req.param("name"));
4450
+ if (!found) throw new HttpError(404, `skill \u4E0D\u5B58\u5728\uFF1A${c.req.param("name")}`);
4451
+ const { skill, disabled } = found;
4452
+ const loaded = loadSkillContent(skill);
4453
+ return c.json(ok({
4454
+ name: skill.name,
4455
+ description: skill.description,
4456
+ scope: skill.scope,
4457
+ enabled: !disabled.has(skill.name),
4458
+ path: skill.skillFile,
4459
+ ...skill.license ? { license: skill.license } : {},
4460
+ ...skill.metadata ? { metadata: skill.metadata } : {},
4461
+ content: loaded.content,
4462
+ contentTruncated: loaded.contentTruncated,
4463
+ resources: loaded.resources
4464
+ }));
4465
+ });
4466
+ app.get("/skills/:name/file", (c) => {
4467
+ const found = findSkill(c.req.param("name"));
4468
+ if (!found) throw new HttpError(404, `skill \u4E0D\u5B58\u5728\uFF1A${c.req.param("name")}`);
4469
+ const rel = (c.req.query("path") ?? "").trim().replaceAll("\\", "/").replace(/^\/+/, "");
4470
+ if (!rel) throw new HttpError(400, "\u7F3A\u5C11 path \u53C2\u6570");
4471
+ const root = found.skill.root;
4472
+ const target = path16.resolve(root, rel);
4473
+ if (!target.startsWith(root + path16.sep)) {
4474
+ throw new HttpError(400, "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8 skill \u76EE\u5F55\u5185\u7684\u6587\u4EF6");
4475
+ }
4476
+ let stat;
4477
+ try {
4478
+ stat = fs15.lstatSync(target);
4479
+ } catch {
4480
+ throw new HttpError(404, `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${rel}`);
4481
+ }
4482
+ if (stat.isSymbolicLink() || !stat.isFile()) {
4483
+ throw new HttpError(400, "\u4EC5\u652F\u6301\u8BFB\u53D6\u666E\u901A\u6587\u4EF6");
4484
+ }
4485
+ const truncated = stat.size > SKILL_FILE_VIEW_MAX_BYTES;
4486
+ const data = Buffer.alloc(Math.min(stat.size, SKILL_FILE_VIEW_MAX_BYTES));
4487
+ const fd = fs15.openSync(target, "r");
4488
+ try {
4489
+ fs15.readSync(fd, data, 0, data.length, 0);
4490
+ } finally {
4491
+ fs15.closeSync(fd);
4492
+ }
4493
+ return c.json(ok({ path: rel, content: data.toString("utf-8"), truncated }));
4494
+ });
4406
4495
  app.get("/health", (c) => c.json(ok({ status: "ok" })));
4407
4496
  return app;
4408
4497
  }
4409
4498
 
4410
4499
  // src/server/session-index.ts
4411
- import fs15 from "fs";
4412
- import path16 from "path";
4500
+ import fs16 from "fs";
4501
+ import path17 from "path";
4413
4502
  var HEARTBEAT_TIMEOUT_MS = 3e4;
4414
4503
  var SessionIndex = class {
4415
4504
  file;
@@ -4421,7 +4510,7 @@ var SessionIndex = class {
4421
4510
  /** 启动时加载;文件不存在/损坏则从空开始(rebuild 会补齐) */
4422
4511
  load() {
4423
4512
  try {
4424
- const raw = JSON.parse(fs15.readFileSync(this.file, "utf-8"));
4513
+ const raw = JSON.parse(fs16.readFileSync(this.file, "utf-8"));
4425
4514
  for (const meta of raw.sessions ?? []) this.metas.set(meta.session_id, meta);
4426
4515
  } catch {
4427
4516
  this.metas.clear();
@@ -4533,21 +4622,21 @@ var SessionIndex = class {
4533
4622
  this.persist();
4534
4623
  }
4535
4624
  persist() {
4536
- const dir = path16.dirname(this.file);
4537
- fs15.mkdirSync(dir, { recursive: true });
4625
+ const dir = path17.dirname(this.file);
4626
+ fs16.mkdirSync(dir, { recursive: true });
4538
4627
  const tmp = `${this.file}.tmp`;
4539
- fs15.writeFileSync(tmp, JSON.stringify({ sessions: [...this.metas.values()] }, null, 2));
4540
- fs15.renameSync(tmp, this.file);
4628
+ fs16.writeFileSync(tmp, JSON.stringify({ sessions: [...this.metas.values()] }, null, 2));
4629
+ fs16.renameSync(tmp, this.file);
4541
4630
  }
4542
4631
  };
4543
4632
 
4544
4633
  // src/server/index.ts
4545
- import path17 from "path";
4634
+ import path18 from "path";
4546
4635
  import { serve } from "@hono/node-server";
4547
4636
 
4548
4637
  // src/server/config-routes.ts
4549
4638
  import { Hono as Hono2 } from "hono";
4550
- import fs16 from "fs";
4639
+ import fs17 from "fs";
4551
4640
  import { z as z15 } from "zod";
4552
4641
  function maskApiKey(key) {
4553
4642
  if (key.includes("${")) return key;
@@ -4607,11 +4696,12 @@ var putPayloadSchema = z15.object({
4607
4696
  enable_command_execution: z15.boolean().optional(),
4608
4697
  enable_agents_md: z15.boolean().optional(),
4609
4698
  enable_skills: z15.boolean().optional(),
4699
+ disabled_skills: z15.array(z15.string()).optional(),
4610
4700
  skills_dirs: z15.array(z15.string()).optional()
4611
4701
  }).optional()
4612
4702
  });
4613
4703
  function loadRawConfig(configPath) {
4614
- const raw = JSON.parse(fs16.readFileSync(configPath, "utf-8"));
4704
+ const raw = JSON.parse(fs17.readFileSync(configPath, "utf-8"));
4615
4705
  return appConfigSchema.parse(raw);
4616
4706
  }
4617
4707
  function createConfigRoutes(ctx) {
@@ -4697,19 +4787,29 @@ function createConfigRoutes(ctx) {
4697
4787
 
4698
4788
  // src/server/index.ts
4699
4789
  function createAgentServer(config, opts = {}) {
4700
- const dataDir = path17.resolve(opts.dataDir ?? defaultDataDir());
4701
- const configPath = opts.configPath ?? path17.join(dataDir, "config.json");
4790
+ const dataDir = path18.resolve(opts.dataDir ?? defaultDataDir());
4791
+ const configPath = opts.configPath ?? path18.join(dataDir, "config.json");
4702
4792
  const resolver = new EndpointResolver(config.endpoints);
4703
- const store = new AgentSessionStore(path17.join(dataDir, "sessions"));
4704
- const index = new SessionIndex(path17.join(dataDir, "index.json"));
4793
+ const store = new AgentSessionStore(path18.join(dataDir, "sessions"));
4794
+ const index = new SessionIndex(path18.join(dataDir, "index.json"));
4705
4795
  index.rebuild(store.scanAll());
4706
4796
  const registry = new RunRegistry();
4707
- const workdir = path17.resolve(config.agent?.workdir ?? path17.join(dataDir, "workspace"));
4797
+ const workdir = path18.resolve(config.agent?.workdir ?? path18.join(dataDir, "workspace"));
4708
4798
  const grants = new EscalationGrants();
4709
- const workspaceContext = {
4710
- enableAgentsMd: config.agent?.enable_agents_md !== false,
4711
- enableSkills: config.agent?.enable_skills !== false,
4712
- ...config.agent?.skills_dirs?.length ? { skills: { extraDirs: config.agent.skills_dirs } } : {}
4799
+ const buildWorkspaceContext = (agentConfig) => ({
4800
+ enableAgentsMd: agentConfig?.enable_agents_md !== false,
4801
+ enableSkills: agentConfig?.enable_skills !== false,
4802
+ ...agentConfig?.disabled_skills?.length ? { disabledSkills: agentConfig.disabled_skills } : {},
4803
+ ...agentConfig?.skills_dirs?.length ? { skills: { extraDirs: agentConfig.skills_dirs } } : {}
4804
+ });
4805
+ const startupWorkspaceContext = buildWorkspaceContext(config.agent);
4806
+ const resolveWorkspaceContext = () => {
4807
+ try {
4808
+ return buildWorkspaceContext(loadConfig(configPath).agent);
4809
+ } catch (error) {
4810
+ console.warn(`[workspace-context] \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u6CBF\u7528\u542F\u52A8\u5FEB\u7167\uFF1A${error instanceof Error ? error.message : error}`);
4811
+ return startupWorkspaceContext;
4812
+ }
4713
4813
  };
4714
4814
  const app = createAgentRoutes({
4715
4815
  resolver,
@@ -4725,7 +4825,8 @@ function createAgentServer(config, opts = {}) {
4725
4825
  ...skills?.length ? { skills } : {}
4726
4826
  }),
4727
4827
  workdir,
4728
- workspaceContext,
4828
+ workspaceContext: startupWorkspaceContext,
4829
+ resolveWorkspaceContext,
4729
4830
  ...config.agent?.agent_name ? { agentName: config.agent.agent_name } : {},
4730
4831
  ...config.agent?.persona ? { persona: config.agent.persona } : {},
4731
4832
  ...config.agent?.agent_instance_id ? { agentInstanceId: config.agent.agent_instance_id } : {},
@@ -4745,13 +4846,14 @@ function createAgentServer(config, opts = {}) {
4745
4846
  return { app, registry, store, index };
4746
4847
  }
4747
4848
  function serveFromConfig(opts) {
4748
- const configPath = opts.configPath ?? path17.join(defaultDataDir(), "config.json");
4849
+ const dataDir = opts.dataDir ? path18.resolve(opts.dataDir) : defaultDataDir();
4850
+ const configPath = opts.configPath ?? path18.join(dataDir, "config.json");
4749
4851
  const config = loadConfig(configPath);
4750
- const { app } = createAgentServer(config, { configPath });
4852
+ const { app } = createAgentServer(config, { configPath, dataDir });
4751
4853
  const port = opts.port ?? config.server?.port ?? 3210;
4752
4854
  const server = serve({ fetch: app.fetch, port }, (info) => {
4753
4855
  console.log(`xlyra-agent \u670D\u52A1\u5DF2\u542F\u52A8\uFF1Ahttp://127.0.0.1:${info.port}`);
4754
- console.log(`\u6570\u636E\u76EE\u5F55\uFF1A${defaultDataDir()}\uFF08sessions/ \u4F1A\u8BDD\u8F6C\u5F55\uFF0Cworkspace/ \u5DE5\u5177\u5DE5\u4F5C\u533A\uFF09`);
4856
+ console.log(`\u6570\u636E\u76EE\u5F55\uFF1A${dataDir}\uFF08sessions/ \u4F1A\u8BDD\u8F6C\u5F55\uFF0Cworkspace/ \u5DE5\u5177\u5DE5\u4F5C\u533A\uFF09`);
4755
4857
  if (!config.server?.token) {
4756
4858
  console.warn("\u26A0 \u672A\u914D\u7F6E server.token\uFF0C\u63A5\u53E3\u65E0\u9274\u6743\u2014\u2014\u8BF7\u52FF\u66B4\u9732\u5230\u516C\u7F51");
4757
4859
  }
@@ -4869,4 +4971,4 @@ export {
4869
4971
  createAgentServer,
4870
4972
  serveFromConfig
4871
4973
  };
4872
- //# sourceMappingURL=chunk-LQRXFYA3.js.map
4974
+ //# sourceMappingURL=chunk-5AIZKDJO.js.map