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

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
  }
@@ -4278,6 +4296,12 @@ function createAgentRoutes(ctx) {
4278
4296
  if (!esc || esc.type !== "escalation") fail(404, "\u63D0\u6743\u8BF7\u6C42\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F");
4279
4297
  if (esc.resolved_path !== resolved_path) fail(400, "\u6388\u6743\u8DEF\u5F84\u4E0E\u63D0\u6743\u8BF7\u6C42\u4E0D\u4E00\u81F4");
4280
4298
  if (esc.granted !== null && esc.granted !== void 0) fail(400, "\u8BE5\u63D0\u6743\u8BF7\u6C42\u5DF2\u5904\u7406\u8FC7");
4299
+ const lastModelEntry = [...entries].reverse().find(
4300
+ (entry) => isMessageEntry(entry) && Boolean(entry.model?.trim())
4301
+ );
4302
+ const model = lastModelEntry && isMessageEntry(lastModelEntry) ? lastModelEntry.model?.trim() ?? "" : "";
4303
+ if (!model) fail(400, "\u65E0\u6CD5\u786E\u5B9A\u4F1A\u8BDD\u4F7F\u7528\u7684\u6A21\u578B\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F");
4304
+ ctx.resolver.resolve(model);
4281
4305
  ctx.store.resolveEscalation(sessionId, escalation_id, granted);
4282
4306
  if (!granted) {
4283
4307
  const history2 = ctx.store.buildHistoryBeforeEscalation(sessionId, escalation_id);
@@ -4315,7 +4339,7 @@ function createAgentRoutes(ctx) {
4315
4339
  const { runId: runId2 } = await launchUserMessage({
4316
4340
  sessionId,
4317
4341
  content: "",
4318
- model: "",
4342
+ model,
4319
4343
  history: history2,
4320
4344
  entryCount: entries.length,
4321
4345
  recordUser: false
@@ -4331,12 +4355,11 @@ function createAgentRoutes(ctx) {
4331
4355
  (e) => isMessageEntry(e) && e.message.role === "user"
4332
4356
  );
4333
4357
  if (!lastUser || !isMessageEntry(lastUser)) fail(400, "\u4F1A\u8BDD\u4E2D\u6CA1\u6709\u53EF\u7EED\u8DD1\u7684\u7528\u6237\u6D88\u606F");
4334
- ctx.resolver.resolve("");
4335
4358
  const history = ctx.store.buildHistoryBeforeEscalation(sessionId, escalation_id);
4336
4359
  const { messageId, runId } = await launchUserMessage({
4337
4360
  sessionId,
4338
4361
  content: "",
4339
- model: "",
4362
+ model,
4340
4363
  history,
4341
4364
  entryCount: entries.length,
4342
4365
  recordUser: false
@@ -4403,13 +4426,84 @@ data: ${JSON.stringify(payload)}
4403
4426
  await ctx.registry.cancelSession(c.req.param("id"));
4404
4427
  return c.json(ok({}, "\u5DF2\u8BF7\u6C42\u505C\u6B62\u4F1A\u8BDD"));
4405
4428
  });
4429
+ app.get("/skills", (c) => {
4430
+ const opts = currentWorkspaceOptions();
4431
+ const disabled = new Set(opts.disabledSkills ?? []);
4432
+ const discovered = discoverSkills(ctx.workdir, opts.skills ?? {});
4433
+ return c.json(ok({
4434
+ enabled: opts.enableSkills !== false,
4435
+ skills: discovered.skills.map((skill) => ({
4436
+ name: skill.name,
4437
+ description: skill.description,
4438
+ scope: skill.scope,
4439
+ enabled: !disabled.has(skill.name),
4440
+ path: skill.skillFile,
4441
+ ...skill.license ? { license: skill.license } : {}
4442
+ })),
4443
+ warnings: discovered.warnings
4444
+ }));
4445
+ });
4446
+ const SKILL_FILE_VIEW_MAX_BYTES = 256e3;
4447
+ const findSkill = (name) => {
4448
+ const opts = currentWorkspaceOptions();
4449
+ const discovered = discoverSkills(ctx.workdir, opts.skills ?? {});
4450
+ const skill = discovered.skills.find((item) => item.name === name);
4451
+ return skill ? { skill, disabled: new Set(opts.disabledSkills ?? []) } : null;
4452
+ };
4453
+ app.get("/skills/:name", (c) => {
4454
+ const found = findSkill(c.req.param("name"));
4455
+ if (!found) throw new HttpError(404, `skill \u4E0D\u5B58\u5728\uFF1A${c.req.param("name")}`);
4456
+ const { skill, disabled } = found;
4457
+ const loaded = loadSkillContent(skill);
4458
+ return c.json(ok({
4459
+ name: skill.name,
4460
+ description: skill.description,
4461
+ scope: skill.scope,
4462
+ enabled: !disabled.has(skill.name),
4463
+ path: skill.skillFile,
4464
+ ...skill.license ? { license: skill.license } : {},
4465
+ ...skill.metadata ? { metadata: skill.metadata } : {},
4466
+ content: loaded.content,
4467
+ contentTruncated: loaded.contentTruncated,
4468
+ resources: loaded.resources
4469
+ }));
4470
+ });
4471
+ app.get("/skills/:name/file", (c) => {
4472
+ const found = findSkill(c.req.param("name"));
4473
+ if (!found) throw new HttpError(404, `skill \u4E0D\u5B58\u5728\uFF1A${c.req.param("name")}`);
4474
+ const rel = (c.req.query("path") ?? "").trim().replaceAll("\\", "/").replace(/^\/+/, "");
4475
+ if (!rel) throw new HttpError(400, "\u7F3A\u5C11 path \u53C2\u6570");
4476
+ const root = found.skill.root;
4477
+ const target = path16.resolve(root, rel);
4478
+ if (!target.startsWith(root + path16.sep)) {
4479
+ throw new HttpError(400, "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8 skill \u76EE\u5F55\u5185\u7684\u6587\u4EF6");
4480
+ }
4481
+ let stat;
4482
+ try {
4483
+ stat = fs15.lstatSync(target);
4484
+ } catch {
4485
+ throw new HttpError(404, `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${rel}`);
4486
+ }
4487
+ if (stat.isSymbolicLink() || !stat.isFile()) {
4488
+ throw new HttpError(400, "\u4EC5\u652F\u6301\u8BFB\u53D6\u666E\u901A\u6587\u4EF6");
4489
+ }
4490
+ const truncated = stat.size > SKILL_FILE_VIEW_MAX_BYTES;
4491
+ const data = Buffer.alloc(Math.min(stat.size, SKILL_FILE_VIEW_MAX_BYTES));
4492
+ const fd = fs15.openSync(target, "r");
4493
+ try {
4494
+ fs15.readSync(fd, data, 0, data.length, 0);
4495
+ } finally {
4496
+ fs15.closeSync(fd);
4497
+ }
4498
+ return c.json(ok({ path: rel, content: data.toString("utf-8"), truncated }));
4499
+ });
4406
4500
  app.get("/health", (c) => c.json(ok({ status: "ok" })));
4407
4501
  return app;
4408
4502
  }
4409
4503
 
4410
4504
  // src/server/session-index.ts
4411
- import fs15 from "fs";
4412
- import path16 from "path";
4505
+ import fs16 from "fs";
4506
+ import path17 from "path";
4413
4507
  var HEARTBEAT_TIMEOUT_MS = 3e4;
4414
4508
  var SessionIndex = class {
4415
4509
  file;
@@ -4421,7 +4515,7 @@ var SessionIndex = class {
4421
4515
  /** 启动时加载;文件不存在/损坏则从空开始(rebuild 会补齐) */
4422
4516
  load() {
4423
4517
  try {
4424
- const raw = JSON.parse(fs15.readFileSync(this.file, "utf-8"));
4518
+ const raw = JSON.parse(fs16.readFileSync(this.file, "utf-8"));
4425
4519
  for (const meta of raw.sessions ?? []) this.metas.set(meta.session_id, meta);
4426
4520
  } catch {
4427
4521
  this.metas.clear();
@@ -4533,21 +4627,21 @@ var SessionIndex = class {
4533
4627
  this.persist();
4534
4628
  }
4535
4629
  persist() {
4536
- const dir = path16.dirname(this.file);
4537
- fs15.mkdirSync(dir, { recursive: true });
4630
+ const dir = path17.dirname(this.file);
4631
+ fs16.mkdirSync(dir, { recursive: true });
4538
4632
  const tmp = `${this.file}.tmp`;
4539
- fs15.writeFileSync(tmp, JSON.stringify({ sessions: [...this.metas.values()] }, null, 2));
4540
- fs15.renameSync(tmp, this.file);
4633
+ fs16.writeFileSync(tmp, JSON.stringify({ sessions: [...this.metas.values()] }, null, 2));
4634
+ fs16.renameSync(tmp, this.file);
4541
4635
  }
4542
4636
  };
4543
4637
 
4544
4638
  // src/server/index.ts
4545
- import path17 from "path";
4639
+ import path18 from "path";
4546
4640
  import { serve } from "@hono/node-server";
4547
4641
 
4548
4642
  // src/server/config-routes.ts
4549
4643
  import { Hono as Hono2 } from "hono";
4550
- import fs16 from "fs";
4644
+ import fs17 from "fs";
4551
4645
  import { z as z15 } from "zod";
4552
4646
  function maskApiKey(key) {
4553
4647
  if (key.includes("${")) return key;
@@ -4607,11 +4701,12 @@ var putPayloadSchema = z15.object({
4607
4701
  enable_command_execution: z15.boolean().optional(),
4608
4702
  enable_agents_md: z15.boolean().optional(),
4609
4703
  enable_skills: z15.boolean().optional(),
4704
+ disabled_skills: z15.array(z15.string()).optional(),
4610
4705
  skills_dirs: z15.array(z15.string()).optional()
4611
4706
  }).optional()
4612
4707
  });
4613
4708
  function loadRawConfig(configPath) {
4614
- const raw = JSON.parse(fs16.readFileSync(configPath, "utf-8"));
4709
+ const raw = JSON.parse(fs17.readFileSync(configPath, "utf-8"));
4615
4710
  return appConfigSchema.parse(raw);
4616
4711
  }
4617
4712
  function createConfigRoutes(ctx) {
@@ -4697,19 +4792,29 @@ function createConfigRoutes(ctx) {
4697
4792
 
4698
4793
  // src/server/index.ts
4699
4794
  function createAgentServer(config, opts = {}) {
4700
- const dataDir = path17.resolve(opts.dataDir ?? defaultDataDir());
4701
- const configPath = opts.configPath ?? path17.join(dataDir, "config.json");
4795
+ const dataDir = path18.resolve(opts.dataDir ?? defaultDataDir());
4796
+ const configPath = opts.configPath ?? path18.join(dataDir, "config.json");
4702
4797
  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"));
4798
+ const store = new AgentSessionStore(path18.join(dataDir, "sessions"));
4799
+ const index = new SessionIndex(path18.join(dataDir, "index.json"));
4705
4800
  index.rebuild(store.scanAll());
4706
4801
  const registry = new RunRegistry();
4707
- const workdir = path17.resolve(config.agent?.workdir ?? path17.join(dataDir, "workspace"));
4802
+ const workdir = path18.resolve(config.agent?.workdir ?? path18.join(dataDir, "workspace"));
4708
4803
  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 } } : {}
4804
+ const buildWorkspaceContext = (agentConfig) => ({
4805
+ enableAgentsMd: agentConfig?.enable_agents_md !== false,
4806
+ enableSkills: agentConfig?.enable_skills !== false,
4807
+ ...agentConfig?.disabled_skills?.length ? { disabledSkills: agentConfig.disabled_skills } : {},
4808
+ ...agentConfig?.skills_dirs?.length ? { skills: { extraDirs: agentConfig.skills_dirs } } : {}
4809
+ });
4810
+ const startupWorkspaceContext = buildWorkspaceContext(config.agent);
4811
+ const resolveWorkspaceContext = () => {
4812
+ try {
4813
+ return buildWorkspaceContext(loadConfig(configPath).agent);
4814
+ } catch (error) {
4815
+ console.warn(`[workspace-context] \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u6CBF\u7528\u542F\u52A8\u5FEB\u7167\uFF1A${error instanceof Error ? error.message : error}`);
4816
+ return startupWorkspaceContext;
4817
+ }
4713
4818
  };
4714
4819
  const app = createAgentRoutes({
4715
4820
  resolver,
@@ -4725,7 +4830,8 @@ function createAgentServer(config, opts = {}) {
4725
4830
  ...skills?.length ? { skills } : {}
4726
4831
  }),
4727
4832
  workdir,
4728
- workspaceContext,
4833
+ workspaceContext: startupWorkspaceContext,
4834
+ resolveWorkspaceContext,
4729
4835
  ...config.agent?.agent_name ? { agentName: config.agent.agent_name } : {},
4730
4836
  ...config.agent?.persona ? { persona: config.agent.persona } : {},
4731
4837
  ...config.agent?.agent_instance_id ? { agentInstanceId: config.agent.agent_instance_id } : {},
@@ -4745,13 +4851,14 @@ function createAgentServer(config, opts = {}) {
4745
4851
  return { app, registry, store, index };
4746
4852
  }
4747
4853
  function serveFromConfig(opts) {
4748
- const configPath = opts.configPath ?? path17.join(defaultDataDir(), "config.json");
4854
+ const dataDir = opts.dataDir ? path18.resolve(opts.dataDir) : defaultDataDir();
4855
+ const configPath = opts.configPath ?? path18.join(dataDir, "config.json");
4749
4856
  const config = loadConfig(configPath);
4750
- const { app } = createAgentServer(config, { configPath });
4857
+ const { app } = createAgentServer(config, { configPath, dataDir });
4751
4858
  const port = opts.port ?? config.server?.port ?? 3210;
4752
4859
  const server = serve({ fetch: app.fetch, port }, (info) => {
4753
4860
  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`);
4861
+ console.log(`\u6570\u636E\u76EE\u5F55\uFF1A${dataDir}\uFF08sessions/ \u4F1A\u8BDD\u8F6C\u5F55\uFF0Cworkspace/ \u5DE5\u5177\u5DE5\u4F5C\u533A\uFF09`);
4755
4862
  if (!config.server?.token) {
4756
4863
  console.warn("\u26A0 \u672A\u914D\u7F6E server.token\uFF0C\u63A5\u53E3\u65E0\u9274\u6743\u2014\u2014\u8BF7\u52FF\u66B4\u9732\u5230\u516C\u7F51");
4757
4864
  }
@@ -4869,4 +4976,4 @@ export {
4869
4976
  createAgentServer,
4870
4977
  serveFromConfig
4871
4978
  };
4872
- //# sourceMappingURL=chunk-LQRXFYA3.js.map
4979
+ //# sourceMappingURL=chunk-BOTYL4W5.js.map