@cremini/skillpack 1.2.7 → 1.2.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/cli.js CHANGED
@@ -9,212 +9,138 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/runtime/config.ts
13
- import fs5 from "fs";
14
- import path5 from "path";
15
- var SUPPORTED_PROVIDERS, ConfigManager, configManager, ConfigFileAuthBackend;
16
- var init_config = __esm({
17
- "src/runtime/config.ts"() {
18
- "use strict";
19
- SUPPORTED_PROVIDERS = {
20
- openai: {
21
- label: "OpenAI",
22
- defaultModelId: "gpt-5.4",
23
- authType: "api_key",
24
- envKey: "OPENAI_API_KEY",
25
- placeholder: "sk-proj-...",
26
- baseUrlPlaceholder: "https://api.openai.com/v1",
27
- supportsBaseUrl: true
28
- },
29
- anthropic: {
30
- label: "Anthropic",
31
- defaultModelId: "claude-opus-4-6",
32
- authType: "api_key",
33
- envKey: "ANTHROPIC_API_KEY",
34
- placeholder: "sk-ant-api03-...",
35
- baseUrlPlaceholder: "https://api.anthropic.com",
36
- supportsBaseUrl: true
37
- },
38
- google: {
39
- label: "Google (Gemini)",
40
- defaultModelId: "gemini-2.5-pro",
41
- authType: "api_key",
42
- envKey: "GOOGLE_API_KEY",
43
- placeholder: "AIza...",
44
- supportsBaseUrl: false
12
+ // src/job-config.ts
13
+ import fs2 from "fs";
14
+ import path2 from "path";
15
+ function getJobFilePath(workDir) {
16
+ return path2.join(workDir, JOB_FILE);
17
+ }
18
+ function validateScheduledJobConfig(value, sourceLabel, index) {
19
+ if (!value || typeof value !== "object") {
20
+ throw new Error(
21
+ `Invalid job config from ${sourceLabel}: "jobs[${index}]" must be an object`
22
+ );
23
+ }
24
+ const job = value;
25
+ if (typeof job.name !== "string" || !job.name.trim()) {
26
+ throw new Error(
27
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].name" is required`
28
+ );
29
+ }
30
+ if (typeof job.cron !== "string" || !job.cron.trim()) {
31
+ throw new Error(
32
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].cron" is required`
33
+ );
34
+ }
35
+ if (typeof job.prompt !== "string" || !job.prompt.trim()) {
36
+ throw new Error(
37
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].prompt" is required`
38
+ );
39
+ }
40
+ if (!job.notify || typeof job.notify !== "object") {
41
+ throw new Error(
42
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].notify" must be an object`
43
+ );
44
+ }
45
+ const notify = job.notify;
46
+ if (typeof notify.adapter !== "string" || !notify.adapter.trim()) {
47
+ throw new Error(
48
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].notify.adapter" is required`
49
+ );
50
+ }
51
+ if (typeof notify.channelId !== "string" || !notify.channelId.trim()) {
52
+ throw new Error(
53
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].notify.channelId" is required`
54
+ );
55
+ }
56
+ if (job.enabled !== void 0 && typeof job.enabled !== "boolean") {
57
+ throw new Error(
58
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].enabled" must be a boolean`
59
+ );
60
+ }
61
+ if (job.timezone !== void 0 && typeof job.timezone !== "string") {
62
+ throw new Error(
63
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].timezone" must be a string`
64
+ );
65
+ }
66
+ }
67
+ function validateJobFileShape(value, sourceLabel) {
68
+ if (!value || typeof value !== "object") {
69
+ throw new Error(`Invalid job config from ${sourceLabel}: expected a JSON object`);
70
+ }
71
+ const jobFile = value;
72
+ if (!Array.isArray(jobFile.jobs)) {
73
+ throw new Error(`Invalid job config from ${sourceLabel}: "jobs" must be an array`);
74
+ }
75
+ const names = /* @__PURE__ */ new Set();
76
+ jobFile.jobs.forEach((job, index) => {
77
+ validateScheduledJobConfig(job, sourceLabel, index);
78
+ const normalizedName = job.name.trim().toLowerCase();
79
+ if (names.has(normalizedName)) {
80
+ throw new Error(
81
+ `Invalid job config from ${sourceLabel}: duplicate job name "${job.name}" is not allowed`
82
+ );
83
+ }
84
+ names.add(normalizedName);
85
+ });
86
+ }
87
+ function normalizeJobFile(jobFile) {
88
+ return {
89
+ jobs: jobFile.jobs.map((job) => ({
90
+ name: job.name.trim(),
91
+ cron: job.cron.trim(),
92
+ prompt: job.prompt,
93
+ notify: {
94
+ adapter: job.notify.adapter.trim(),
95
+ channelId: job.notify.channelId.trim()
45
96
  },
46
- "openai-codex": {
47
- label: "OpenAI Codex",
48
- defaultModelId: "gpt-5.4",
49
- authType: "oauth",
50
- oauthProviderId: "openai-codex",
51
- supportsBaseUrl: false
52
- }
53
- };
54
- ConfigManager = class _ConfigManager {
55
- static instance;
56
- configData = {};
57
- configPath = "";
58
- constructor() {
59
- }
60
- static getInstance() {
61
- if (!_ConfigManager.instance) {
62
- _ConfigManager.instance = new _ConfigManager();
63
- }
64
- return _ConfigManager.instance;
65
- }
66
- load(rootDir) {
67
- this.configPath = path5.join(rootDir, "data", "config.json");
68
- if (fs5.existsSync(this.configPath)) {
69
- try {
70
- this.configData = JSON.parse(fs5.readFileSync(this.configPath, "utf-8"));
71
- console.log(" Loaded config from data/config.json");
72
- } catch (err) {
73
- console.warn(" Warning: Failed to parse data/config.json:", err);
74
- }
75
- }
76
- let { apiKey = "", provider = "openai", baseUrl = "" } = this.configData;
77
- if (!apiKey) {
78
- if (process.env.OPENAI_API_KEY) {
79
- apiKey = process.env.OPENAI_API_KEY;
80
- provider = "openai";
81
- } else if (process.env.ANTHROPIC_API_KEY) {
82
- apiKey = process.env.ANTHROPIC_API_KEY;
83
- provider = "anthropic";
84
- } else if (process.env.GOOGLE_API_KEY) {
85
- apiKey = process.env.GOOGLE_API_KEY;
86
- provider = "google";
87
- }
88
- }
89
- this.configData.apiKey = apiKey;
90
- this.configData.provider = provider;
91
- this.configData.baseUrl = baseUrl?.trim() || void 0;
92
- return this.configData;
93
- }
94
- getConfig() {
95
- return this.configData;
96
- }
97
- save(rootDir, updates) {
98
- const configDir = path5.join(rootDir, "data");
99
- if (!this.configPath) {
100
- this.configPath = path5.join(rootDir, "data", "config.json");
101
- }
102
- if (!fs5.existsSync(configDir)) {
103
- fs5.mkdirSync(configDir, { recursive: true });
104
- }
105
- if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
106
- if (updates.provider !== void 0) this.configData.provider = updates.provider;
107
- if (updates.baseUrl !== void 0) {
108
- this.configData.baseUrl = updates.baseUrl?.trim() || void 0;
109
- }
110
- if (updates.modelId !== void 0) {
111
- this.configData.modelId = updates.modelId?.trim() || void 0;
112
- }
113
- if (updates.apiProtocol !== void 0) {
114
- this.configData.apiProtocol = updates.apiProtocol || void 0;
115
- }
116
- if (updates.adapters !== void 0) {
117
- const merged = { ...this.configData.adapters || {} };
118
- for (const [adapterKey, adapterVal] of Object.entries(updates.adapters)) {
119
- if (adapterVal === null || adapterVal === void 0) {
120
- delete merged[adapterKey];
121
- } else {
122
- merged[adapterKey] = adapterVal;
123
- }
124
- }
125
- this.configData.adapters = merged;
126
- }
127
- if (updates.scheduledJobs !== void 0) {
128
- this.configData.scheduledJobs = updates.scheduledJobs;
129
- }
130
- try {
131
- fs5.writeFileSync(
132
- this.configPath,
133
- JSON.stringify(this.configData, null, 2),
134
- "utf-8"
135
- );
136
- } catch (err) {
137
- console.error("Failed to save config:", err);
138
- }
139
- }
140
- };
141
- configManager = ConfigManager.getInstance();
142
- ConfigFileAuthBackend = class {
143
- constructor(configPath) {
144
- this.configPath = configPath;
145
- }
146
- ensureFile() {
147
- const dir = path5.dirname(this.configPath);
148
- if (!fs5.existsSync(dir)) {
149
- fs5.mkdirSync(dir, { recursive: true });
150
- }
151
- if (!fs5.existsSync(this.configPath)) {
152
- fs5.writeFileSync(this.configPath, "{}", "utf-8");
153
- }
154
- }
155
- readAuthJson() {
156
- this.ensureFile();
157
- try {
158
- const raw = fs5.readFileSync(this.configPath, "utf-8");
159
- const config = JSON.parse(raw);
160
- if (config._auth && typeof config._auth === "object") {
161
- return JSON.stringify(config._auth);
162
- }
163
- return void 0;
164
- } catch {
165
- return void 0;
166
- }
167
- }
168
- writeAuthJson(authJson) {
169
- this.ensureFile();
170
- try {
171
- const raw = fs5.readFileSync(this.configPath, "utf-8");
172
- const config = JSON.parse(raw);
173
- config._auth = JSON.parse(authJson);
174
- fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
175
- } catch {
176
- const config = { _auth: JSON.parse(authJson) };
177
- fs5.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
178
- }
179
- }
180
- withLock(fn) {
181
- const current = this.readAuthJson();
182
- const { result, next } = fn(current);
183
- if (next !== void 0) {
184
- this.writeAuthJson(next);
185
- }
186
- return result;
187
- }
188
- async withLockAsync(fn) {
189
- const current = this.readAuthJson();
190
- const { result, next } = await fn(current);
191
- if (next !== void 0) {
192
- this.writeAuthJson(next);
193
- }
194
- return result;
195
- }
196
- };
97
+ ...job.enabled !== void 0 ? { enabled: job.enabled } : {},
98
+ ...job.timezone !== void 0 ? { timezone: job.timezone.trim() } : {}
99
+ }))
100
+ };
101
+ }
102
+ function loadJobFile(workDir) {
103
+ const filePath = getJobFilePath(workDir);
104
+ if (!fs2.existsSync(filePath)) {
105
+ return { jobs: [] };
106
+ }
107
+ const raw = fs2.readFileSync(filePath, "utf-8");
108
+ const parsed = JSON.parse(raw);
109
+ validateJobFileShape(parsed, filePath);
110
+ return normalizeJobFile(parsed);
111
+ }
112
+ function saveJobFile(workDir, jobFile) {
113
+ const filePath = getJobFilePath(workDir);
114
+ const normalized = normalizeJobFile(jobFile);
115
+ validateJobFileShape(normalized, filePath);
116
+ fs2.writeFileSync(filePath, JSON.stringify(normalized, null, 2) + "\n", "utf-8");
117
+ }
118
+ var JOB_FILE;
119
+ var init_job_config = __esm({
120
+ "src/job-config.ts"() {
121
+ "use strict";
122
+ JOB_FILE = "job.json";
197
123
  }
198
124
  });
199
125
 
200
126
  // src/runtime/adapters/attachment-utils.ts
201
- import fs6 from "fs";
202
- import path6 from "path";
127
+ import fs7 from "fs";
128
+ import path7 from "path";
203
129
  import { pipeline } from "stream/promises";
204
130
  import { Readable } from "stream";
205
131
  function getAttachmentDir(rootDir, channelId) {
206
- return path6.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
132
+ return path7.resolve(rootDir, "data", "sessions", channelId, ATTACHMENTS_DIR);
207
133
  }
208
134
  function sanitizeFilename(name) {
209
135
  return name.replace(/[/\\:*?"<>|]/g, "_").replace(/\s+/g, "_");
210
136
  }
211
137
  async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mimeType, headers) {
212
138
  const dir = getAttachmentDir(rootDir, channelId);
213
- fs6.mkdirSync(dir, { recursive: true });
139
+ fs7.mkdirSync(dir, { recursive: true });
214
140
  const ts = Date.now();
215
141
  const safeName = sanitizeFilename(filename);
216
142
  const storedName = `${ts}-${safeName}`;
217
- const fullPath = path6.join(dir, storedName);
143
+ const fullPath = path7.join(dir, storedName);
218
144
  const response = await fetch(url, { headers });
219
145
  if (!response.ok) {
220
146
  throw new Error(
@@ -226,9 +152,9 @@ async function downloadAndSaveAttachment(rootDir, channelId, url, filename, mime
226
152
  throw new Error(`Empty response body when downloading ${url}`);
227
153
  }
228
154
  const nodeStream = Readable.fromWeb(body);
229
- const writeStream = fs6.createWriteStream(fullPath);
155
+ const writeStream = fs7.createWriteStream(fullPath);
230
156
  await pipeline(nodeStream, writeStream);
231
- const stats = fs6.statSync(fullPath);
157
+ const stats = fs7.statSync(fullPath);
232
158
  const detectedMime = mimeType || response.headers.get("content-type")?.split(";")[0] || void 0;
233
159
  return {
234
160
  filename,
@@ -257,7 +183,7 @@ ${lines.join("\n")}`;
257
183
  }
258
184
  function attachmentsToImageContent(attachments) {
259
185
  return attachments.filter((a) => isImageMime(a.mimeType)).map((a) => {
260
- const buffer = fs6.readFileSync(a.localPath);
186
+ const buffer = fs7.readFileSync(a.localPath);
261
187
  return {
262
188
  type: "image",
263
189
  data: buffer.toString("base64"),
@@ -463,7 +389,7 @@ var telegram_exports = {};
463
389
  __export(telegram_exports, {
464
390
  TelegramAdapter: () => TelegramAdapter
465
391
  });
466
- import fs13 from "fs";
392
+ import fs17 from "fs";
467
393
  import TelegramBot from "node-telegram-bot-api";
468
394
  var MAX_MESSAGE_LENGTH, ACK_REACTION, TelegramAdapter;
469
395
  var init_telegram = __esm({
@@ -781,7 +707,7 @@ var init_telegram = __esm({
781
707
  async sendFileSafe(chatId, filePath, caption) {
782
708
  if (!this.bot) return;
783
709
  try {
784
- if (!fs13.existsSync(filePath)) {
710
+ if (!fs17.existsSync(filePath)) {
785
711
  console.error(`[Telegram] File not found for sending: ${filePath}`);
786
712
  return;
787
713
  }
@@ -801,8 +727,8 @@ var slack_exports = {};
801
727
  __export(slack_exports, {
802
728
  SlackAdapter: () => SlackAdapter
803
729
  });
804
- import fs14 from "fs";
805
- import path13 from "path";
730
+ import fs18 from "fs";
731
+ import path17 from "path";
806
732
  import { App, LogLevel } from "@slack/bolt";
807
733
  var INLINE_COMMANDS, SLASH_COMMANDS, MAX_MESSAGE_LENGTH2, ACK_REACTION2, PROCESSING_MESSAGE, SlackAdapter;
808
734
  var init_slack = __esm({
@@ -1437,12 +1363,12 @@ var init_slack = __esm({
1437
1363
  */
1438
1364
  async sendFileSafe(client, route, filePath, caption) {
1439
1365
  try {
1440
- if (!fs14.existsSync(filePath)) {
1366
+ if (!fs18.existsSync(filePath)) {
1441
1367
  console.error(`[Slack] File not found for sending: ${filePath}`);
1442
1368
  return;
1443
1369
  }
1444
- const filename = path13.basename(filePath);
1445
- const fileContent = fs14.readFileSync(filePath);
1370
+ const filename = path17.basename(filePath);
1371
+ const fileContent = fs18.readFileSync(filePath);
1446
1372
  await client.files.uploadV2({
1447
1373
  channel_id: route.channel,
1448
1374
  thread_ts: route.threadTs,
@@ -1480,7 +1406,7 @@ var VALID_JOB_NAME, SchedulerAdapter;
1480
1406
  var init_scheduler = __esm({
1481
1407
  "src/runtime/adapters/scheduler.ts"() {
1482
1408
  "use strict";
1483
- init_config();
1409
+ init_job_config();
1484
1410
  VALID_JOB_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
1485
1411
  SchedulerAdapter = class {
1486
1412
  name = "scheduler";
@@ -1494,8 +1420,7 @@ var init_scheduler = __esm({
1494
1420
  this.rootDir = ctx.rootDir;
1495
1421
  this.notifyFn = ctx.notify || (async () => {
1496
1422
  });
1497
- const config = configManager.getConfig();
1498
- const jobConfigs = config.scheduledJobs || [];
1423
+ const jobConfigs = loadJobFile(this.rootDir).jobs;
1499
1424
  let scheduledCount = 0;
1500
1425
  let disabledCount = 0;
1501
1426
  for (const jc of jobConfigs) {
@@ -1599,11 +1524,13 @@ var init_scheduler = __esm({
1599
1524
  }
1600
1525
  };
1601
1526
  try {
1527
+ await this.clearJobContext(channelId);
1602
1528
  const result = await this.agent.handleMessage(
1603
1529
  "scheduler",
1604
1530
  channelId,
1605
1531
  jobConfig.prompt,
1606
- onEvent
1532
+ onEvent,
1533
+ void 0
1607
1534
  );
1608
1535
  if (result.errorMessage) {
1609
1536
  fullText = `\u274C \u5B9A\u65F6\u4EFB\u52A1 "${jobConfig.name}" \u6267\u884C\u5931\u8D25\uFF1A${result.errorMessage}`;
@@ -1648,11 +1575,17 @@ var init_scheduler = __esm({
1648
1575
  }
1649
1576
  return { text: fullText, notifyFailed };
1650
1577
  }
1578
+ async clearJobContext(channelId) {
1579
+ const result = await this.agent.handleCommand("clear", channelId);
1580
+ if (!result.success) {
1581
+ throw new Error(result.message || `Failed to clear context for ${channelId}`);
1582
+ }
1583
+ }
1651
1584
  // -------------------------------------------------------------------------
1652
1585
  // Dynamic management API
1653
1586
  // -------------------------------------------------------------------------
1654
1587
  /**
1655
- * Add a new job, persist to config.json.
1588
+ * Add a new job, persist to job.json.
1656
1589
  */
1657
1590
  addJob(jobConfig) {
1658
1591
  if (this.jobs.has(jobConfig.name)) {
@@ -1673,7 +1606,7 @@ var init_scheduler = __esm({
1673
1606
  };
1674
1607
  }
1675
1608
  /**
1676
- * Remove a job and persist to config.json.
1609
+ * Remove a job and persist to job.json.
1677
1610
  */
1678
1611
  removeJob(name) {
1679
1612
  if (!this.jobs.has(name)) {
@@ -1683,6 +1616,29 @@ var init_scheduler = __esm({
1683
1616
  this.persistJobs();
1684
1617
  return { success: true, message: `Job "${name}" removed.` };
1685
1618
  }
1619
+ updateJob(name, updates) {
1620
+ const job = this.jobs.get(name);
1621
+ if (!job) {
1622
+ return { success: false, message: `Job "${name}" not found.` };
1623
+ }
1624
+ const nextConfig = {
1625
+ name,
1626
+ cron: updates.cron,
1627
+ prompt: updates.prompt,
1628
+ notify: updates.notify,
1629
+ enabled: updates.enabled,
1630
+ timezone: updates.timezone
1631
+ };
1632
+ const result = this.registerJob(nextConfig);
1633
+ if (!result.registered) {
1634
+ return { success: false, message: result.message };
1635
+ }
1636
+ this.persistJobs();
1637
+ return {
1638
+ success: true,
1639
+ message: `Job "${name}" updated.`
1640
+ };
1641
+ }
1686
1642
  /**
1687
1643
  * Enable or disable a job and persist.
1688
1644
  */
@@ -1774,14 +1730,14 @@ var init_scheduler = __esm({
1774
1730
  }
1775
1731
  }
1776
1732
  /**
1777
- * Persist all current jobs to data/config.json.
1733
+ * Persist all current jobs to job.json.
1778
1734
  */
1779
1735
  persistJobs() {
1780
1736
  const configs = [];
1781
1737
  for (const [, job] of this.jobs) {
1782
1738
  configs.push(job.config);
1783
1739
  }
1784
- configManager.save(this.rootDir, { scheduledJobs: configs });
1740
+ saveJobFile(this.rootDir, { jobs: configs });
1785
1741
  }
1786
1742
  // -------------------------------------------------------------------------
1787
1743
  // Lifecycle
@@ -1802,8 +1758,8 @@ import { Command } from "commander";
1802
1758
  import chalk5 from "chalk";
1803
1759
 
1804
1760
  // src/commands/create.ts
1805
- import fs4 from "fs";
1806
- import path4 from "path";
1761
+ import fs5 from "fs";
1762
+ import path5 from "path";
1807
1763
  import inquirer from "inquirer";
1808
1764
  import chalk3 from "chalk";
1809
1765
 
@@ -1911,22 +1867,23 @@ function configExists(workDir) {
1911
1867
  }
1912
1868
 
1913
1869
  // src/commands/zip.ts
1914
- import fs3 from "fs";
1915
- import path3 from "path";
1870
+ import fs4 from "fs";
1871
+ import path4 from "path";
1916
1872
  import archiver from "archiver";
1917
1873
  import chalk2 from "chalk";
1874
+ init_job_config();
1918
1875
 
1919
1876
  // src/skill-manager.ts
1920
1877
  import { spawnSync } from "child_process";
1921
- import fs2 from "fs";
1922
- import path2 from "path";
1878
+ import fs3 from "fs";
1879
+ import path3 from "path";
1923
1880
  import chalk from "chalk";
1924
1881
  var SKILLS_DIR = "skills";
1925
1882
  function normalizeName(value) {
1926
1883
  return value.trim().toLowerCase();
1927
1884
  }
1928
1885
  function getSkillsDir(workDir) {
1929
- return path2.join(workDir, SKILLS_DIR);
1886
+ return path3.join(workDir, SKILLS_DIR);
1930
1887
  }
1931
1888
  function groupSkillsBySource(skills) {
1932
1889
  const groups = /* @__PURE__ */ new Map();
@@ -1983,13 +1940,13 @@ function installSkills(workDir, skills) {
1983
1940
  function scanInstalledSkills(workDir) {
1984
1941
  const installed = [];
1985
1942
  const skillsDir = getSkillsDir(workDir);
1986
- if (!fs2.existsSync(skillsDir)) {
1943
+ if (!fs3.existsSync(skillsDir)) {
1987
1944
  return installed;
1988
1945
  }
1989
1946
  function visit(dir) {
1990
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
1947
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
1991
1948
  for (const entry of entries) {
1992
- const fullPath = path2.join(dir, entry.name);
1949
+ const fullPath = path3.join(dir, entry.name);
1993
1950
  if (entry.isDirectory()) {
1994
1951
  visit(fullPath);
1995
1952
  continue;
@@ -2008,7 +1965,7 @@ function scanInstalledSkills(workDir) {
2008
1965
  }
2009
1966
  function parseSkillMd(filePath) {
2010
1967
  try {
2011
- const content = fs2.readFileSync(filePath, "utf-8");
1968
+ const content = fs3.readFileSync(filePath, "utf-8");
2012
1969
  const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
2013
1970
  if (!frontmatterMatch) {
2014
1971
  return null;
@@ -2021,7 +1978,7 @@ function parseSkillMd(filePath) {
2021
1978
  return {
2022
1979
  name,
2023
1980
  description: readFrontmatterField(frontmatter, "description") ?? "",
2024
- dir: path2.dirname(filePath)
1981
+ dir: path3.dirname(filePath)
2025
1982
  };
2026
1983
  } catch {
2027
1984
  return null;
@@ -2185,12 +2142,12 @@ async function zipCommand(workDir) {
2185
2142
  const config = loadConfig(workDir);
2186
2143
  const slug = config.name.toLowerCase().replace(/\s+/g, "-");
2187
2144
  const zipName = `${slug}.zip`;
2188
- const zipPath = path3.join(workDir, zipName);
2145
+ const zipPath = path4.join(workDir, zipName);
2189
2146
  installConfiguredSkills(workDir, config);
2190
2147
  syncSkillDescriptions(workDir, config);
2191
2148
  saveConfig(workDir, config);
2192
2149
  console.log(chalk2.blue(`Packaging ${config.name}...`));
2193
- const output = fs3.createWriteStream(zipPath);
2150
+ const output = fs4.createWriteStream(zipPath);
2194
2151
  const archive = archiver("zip", { zlib: { level: 9 } });
2195
2152
  return new Promise((resolve, reject) => {
2196
2153
  output.on("close", () => {
@@ -2207,22 +2164,26 @@ async function zipCommand(workDir) {
2207
2164
  archive.file(getPackPath(workDir), {
2208
2165
  name: `${prefix}/${PACK_FILE}`
2209
2166
  });
2167
+ const jobFilePath = getJobFilePath(workDir);
2168
+ if (fs4.existsSync(jobFilePath)) {
2169
+ archive.file(jobFilePath, { name: `${prefix}/${JOB_FILE}` });
2170
+ }
2210
2171
  for (const file of ["AGENTS.md", "SOUL.md"]) {
2211
- const filePath = path3.join(workDir, file);
2212
- if (fs3.existsSync(filePath)) {
2172
+ const filePath = path4.join(workDir, file);
2173
+ if (fs4.existsSync(filePath)) {
2213
2174
  archive.file(filePath, { name: `${prefix}/${file}` });
2214
2175
  }
2215
2176
  }
2216
- const skillsDir = path3.join(workDir, "skills");
2217
- if (fs3.existsSync(skillsDir)) {
2177
+ const skillsDir = path4.join(workDir, "skills");
2178
+ if (fs4.existsSync(skillsDir)) {
2218
2179
  archive.directory(skillsDir, `${prefix}/skills`);
2219
2180
  }
2220
- const startSh = path3.join(workDir, "start.sh");
2221
- if (fs3.existsSync(startSh)) {
2181
+ const startSh = path4.join(workDir, "start.sh");
2182
+ if (fs4.existsSync(startSh)) {
2222
2183
  archive.file(startSh, { name: `${prefix}/start.sh`, mode: 493 });
2223
2184
  }
2224
- const startBat = path3.join(workDir, "start.bat");
2225
- if (fs3.existsSync(startBat)) {
2185
+ const startBat = path4.join(workDir, "start.bat");
2186
+ if (fs4.existsSync(startBat)) {
2226
2187
  archive.file(startBat, { name: `${prefix}/start.bat` });
2227
2188
  }
2228
2189
  archive.finalize();
@@ -2271,24 +2232,24 @@ async function readConfigSource(source) {
2271
2232
  }
2272
2233
  raw = await response.text();
2273
2234
  } else {
2274
- const filePath = path4.resolve(source);
2275
- raw = fs4.readFileSync(filePath, "utf-8");
2235
+ const filePath = path5.resolve(source);
2236
+ raw = fs5.readFileSync(filePath, "utf-8");
2276
2237
  }
2277
2238
  const parsed = JSON.parse(raw);
2278
2239
  validateConfigShape(parsed, source);
2279
2240
  return parsed;
2280
2241
  }
2281
2242
  function copyStartTemplates(workDir) {
2282
- const templateDir = path4.resolve(
2243
+ const templateDir = path5.resolve(
2283
2244
  new URL("../templates", import.meta.url).pathname
2284
2245
  );
2285
2246
  for (const file of ["start.sh", "start.bat"]) {
2286
- const src = path4.join(templateDir, file);
2287
- const dest = path4.join(workDir, file);
2288
- if (fs4.existsSync(src)) {
2289
- fs4.copyFileSync(src, dest);
2247
+ const src = path5.join(templateDir, file);
2248
+ const dest = path5.join(workDir, file);
2249
+ if (fs5.existsSync(src)) {
2250
+ fs5.copyFileSync(src, dest);
2290
2251
  if (file === "start.sh") {
2291
- fs4.chmodSync(dest, 493);
2252
+ fs5.chmodSync(dest, 493);
2292
2253
  }
2293
2254
  } else {
2294
2255
  console.warn(chalk3.yellow(` [warn] Template not found: ${src}`));
@@ -2296,9 +2257,9 @@ function copyStartTemplates(workDir) {
2296
2257
  }
2297
2258
  }
2298
2259
  async function createCommand(directory, options = {}) {
2299
- const workDir = directory ? path4.resolve(directory) : process.cwd();
2260
+ const workDir = directory ? path5.resolve(directory) : process.cwd();
2300
2261
  if (directory) {
2301
- fs4.mkdirSync(workDir, { recursive: true });
2262
+ fs5.mkdirSync(workDir, { recursive: true });
2302
2263
  }
2303
2264
  if (options.config) {
2304
2265
  await initFromConfig(workDir, options.config);
@@ -2469,24 +2430,23 @@ async function interactiveCreate(workDir) {
2469
2430
  }
2470
2431
 
2471
2432
  // src/commands/run.ts
2472
- import path15 from "path";
2473
- import fs16 from "fs";
2433
+ import path19 from "path";
2434
+ import fs20 from "fs";
2474
2435
  import inquirer2 from "inquirer";
2475
2436
  import chalk4 from "chalk";
2476
2437
 
2477
2438
  // src/runtime/server.ts
2478
2439
  import express from "express";
2479
- import path14 from "path";
2480
- import fs15 from "fs";
2440
+ import path18 from "path";
2441
+ import fs19 from "fs";
2481
2442
  import { fileURLToPath as fileURLToPath2 } from "url";
2482
2443
  import { createServer } from "http";
2483
2444
  import { exec } from "child_process";
2484
2445
 
2485
2446
  // src/runtime/agent.ts
2486
- init_config();
2487
- init_attachment_utils();
2488
- import path9 from "path";
2489
- import fs9 from "fs";
2447
+ import path13 from "path";
2448
+ import fs13 from "fs";
2449
+ import { randomUUID as randomUUID3 } from "crypto";
2490
2450
  import { fileURLToPath } from "url";
2491
2451
  import {
2492
2452
  AuthStorage,
@@ -2497,20 +2457,260 @@ import {
2497
2457
  DefaultResourceLoader
2498
2458
  } from "@mariozechner/pi-coding-agent";
2499
2459
 
2500
- // src/runtime/tools/send-file-tool.ts
2501
- import fs7 from "fs";
2502
- import path7 from "path";
2503
- import { Type } from "@sinclair/typebox";
2504
- var SendFileParams = Type.Object({
2505
- filePath: Type.String({
2506
- description: "Absolute path to the file to send to the user. The file must exist and be readable."
2507
- }),
2508
- caption: Type.Optional(
2509
- Type.String({
2510
- description: "Optional caption or description to accompany the file."
2511
- })
2512
- )
2513
- });
2460
+ // src/runtime/config.ts
2461
+ import fs6 from "fs";
2462
+ import path6 from "path";
2463
+ var SUPPORTED_PROVIDERS = {
2464
+ openai: {
2465
+ label: "OpenAI",
2466
+ defaultModelId: "gpt-5.4",
2467
+ authType: "api_key",
2468
+ envKey: "OPENAI_API_KEY",
2469
+ placeholder: "sk-proj-...",
2470
+ baseUrlPlaceholder: "https://api.openai.com/v1",
2471
+ supportsBaseUrl: true
2472
+ },
2473
+ anthropic: {
2474
+ label: "Anthropic",
2475
+ defaultModelId: "claude-opus-4-6",
2476
+ authType: "api_key",
2477
+ envKey: "ANTHROPIC_API_KEY",
2478
+ placeholder: "sk-ant-api03-...",
2479
+ baseUrlPlaceholder: "https://api.anthropic.com",
2480
+ supportsBaseUrl: true
2481
+ },
2482
+ google: {
2483
+ label: "Google (Gemini)",
2484
+ defaultModelId: "gemini-2.5-pro",
2485
+ authType: "api_key",
2486
+ envKey: "GOOGLE_API_KEY",
2487
+ placeholder: "AIza...",
2488
+ supportsBaseUrl: false
2489
+ },
2490
+ "openai-codex": {
2491
+ label: "OpenAI Codex",
2492
+ defaultModelId: "gpt-5.4",
2493
+ authType: "oauth",
2494
+ oauthProviderId: "openai-codex",
2495
+ supportsBaseUrl: false
2496
+ }
2497
+ };
2498
+ function normalizeDataConfig(value) {
2499
+ if (!value || typeof value !== "object") {
2500
+ return {};
2501
+ }
2502
+ const raw = value;
2503
+ const normalized = {};
2504
+ if (typeof raw.apiKey === "string") {
2505
+ normalized.apiKey = raw.apiKey;
2506
+ }
2507
+ if (typeof raw.provider === "string") {
2508
+ normalized.provider = raw.provider;
2509
+ }
2510
+ if (typeof raw.baseUrl === "string") {
2511
+ normalized.baseUrl = raw.baseUrl;
2512
+ }
2513
+ if (typeof raw.modelId === "string") {
2514
+ normalized.modelId = raw.modelId;
2515
+ }
2516
+ if (raw.apiProtocol === "openai-responses" || raw.apiProtocol === "openai-completions") {
2517
+ normalized.apiProtocol = raw.apiProtocol;
2518
+ }
2519
+ if (raw.adapters && typeof raw.adapters === "object" && !Array.isArray(raw.adapters)) {
2520
+ normalized.adapters = raw.adapters;
2521
+ }
2522
+ if (raw._auth && typeof raw._auth === "object" && !Array.isArray(raw._auth)) {
2523
+ normalized._auth = raw._auth;
2524
+ }
2525
+ return normalized;
2526
+ }
2527
+ var ConfigManager = class _ConfigManager {
2528
+ static instance;
2529
+ configData = {};
2530
+ configPath = "";
2531
+ constructor() {
2532
+ }
2533
+ static getInstance() {
2534
+ if (!_ConfigManager.instance) {
2535
+ _ConfigManager.instance = new _ConfigManager();
2536
+ }
2537
+ return _ConfigManager.instance;
2538
+ }
2539
+ load(rootDir) {
2540
+ this.configPath = path6.join(rootDir, "data", "config.json");
2541
+ this.configData = {};
2542
+ if (fs6.existsSync(this.configPath)) {
2543
+ try {
2544
+ const parsed = JSON.parse(fs6.readFileSync(this.configPath, "utf-8"));
2545
+ if (parsed && typeof parsed === "object" && "scheduledJobs" in parsed) {
2546
+ console.warn(
2547
+ ' Warning: data/config.json contains deprecated "scheduledJobs". Move them to job.json at the pack root; the old field is ignored.'
2548
+ );
2549
+ }
2550
+ this.configData = normalizeDataConfig(parsed);
2551
+ console.log(" Loaded config from data/config.json");
2552
+ } catch (err) {
2553
+ console.warn(" Warning: Failed to parse data/config.json:", err);
2554
+ }
2555
+ }
2556
+ let { apiKey = "", provider = "openai", baseUrl = "" } = this.configData;
2557
+ if (!apiKey) {
2558
+ if (process.env.OPENAI_API_KEY) {
2559
+ apiKey = process.env.OPENAI_API_KEY;
2560
+ provider = "openai";
2561
+ } else if (process.env.ANTHROPIC_API_KEY) {
2562
+ apiKey = process.env.ANTHROPIC_API_KEY;
2563
+ provider = "anthropic";
2564
+ } else if (process.env.GOOGLE_API_KEY) {
2565
+ apiKey = process.env.GOOGLE_API_KEY;
2566
+ provider = "google";
2567
+ }
2568
+ }
2569
+ this.configData.apiKey = apiKey;
2570
+ this.configData.provider = provider;
2571
+ this.configData.baseUrl = baseUrl?.trim() || void 0;
2572
+ return this.configData;
2573
+ }
2574
+ getConfig() {
2575
+ return this.configData;
2576
+ }
2577
+ save(rootDir, updates) {
2578
+ const configDir = path6.join(rootDir, "data");
2579
+ if (!this.configPath) {
2580
+ this.configPath = path6.join(rootDir, "data", "config.json");
2581
+ }
2582
+ if (!fs6.existsSync(configDir)) {
2583
+ fs6.mkdirSync(configDir, { recursive: true });
2584
+ }
2585
+ if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
2586
+ if (updates.provider !== void 0) this.configData.provider = updates.provider;
2587
+ if (updates.baseUrl !== void 0) {
2588
+ this.configData.baseUrl = updates.baseUrl?.trim() || void 0;
2589
+ }
2590
+ if (updates.modelId !== void 0) {
2591
+ this.configData.modelId = updates.modelId?.trim() || void 0;
2592
+ }
2593
+ if (updates.apiProtocol !== void 0) {
2594
+ this.configData.apiProtocol = updates.apiProtocol || void 0;
2595
+ }
2596
+ if (updates.adapters !== void 0) {
2597
+ const merged = { ...this.configData.adapters || {} };
2598
+ for (const [adapterKey, adapterVal] of Object.entries(updates.adapters)) {
2599
+ if (adapterVal === null || adapterVal === void 0) {
2600
+ delete merged[adapterKey];
2601
+ } else {
2602
+ merged[adapterKey] = adapterVal;
2603
+ }
2604
+ }
2605
+ this.configData.adapters = merged;
2606
+ }
2607
+ try {
2608
+ this.configData = normalizeDataConfig(this.configData);
2609
+ fs6.writeFileSync(
2610
+ this.configPath,
2611
+ JSON.stringify(this.configData, null, 2),
2612
+ "utf-8"
2613
+ );
2614
+ } catch (err) {
2615
+ console.error("Failed to save config:", err);
2616
+ }
2617
+ }
2618
+ };
2619
+ var configManager = ConfigManager.getInstance();
2620
+ var ConfigFileAuthBackend = class {
2621
+ constructor(configPath) {
2622
+ this.configPath = configPath;
2623
+ }
2624
+ ensureFile() {
2625
+ const dir = path6.dirname(this.configPath);
2626
+ if (!fs6.existsSync(dir)) {
2627
+ fs6.mkdirSync(dir, { recursive: true });
2628
+ }
2629
+ if (!fs6.existsSync(this.configPath)) {
2630
+ fs6.writeFileSync(this.configPath, "{}", "utf-8");
2631
+ }
2632
+ }
2633
+ readAuthJson() {
2634
+ this.ensureFile();
2635
+ try {
2636
+ const raw = fs6.readFileSync(this.configPath, "utf-8");
2637
+ const config = JSON.parse(raw);
2638
+ if (config._auth && typeof config._auth === "object") {
2639
+ return JSON.stringify(config._auth);
2640
+ }
2641
+ return void 0;
2642
+ } catch {
2643
+ return void 0;
2644
+ }
2645
+ }
2646
+ writeAuthJson(authJson) {
2647
+ this.ensureFile();
2648
+ try {
2649
+ const raw = fs6.readFileSync(this.configPath, "utf-8");
2650
+ const config = JSON.parse(raw);
2651
+ config._auth = JSON.parse(authJson);
2652
+ fs6.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
2653
+ } catch {
2654
+ const config = { _auth: JSON.parse(authJson) };
2655
+ fs6.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
2656
+ }
2657
+ }
2658
+ withLock(fn) {
2659
+ const current = this.readAuthJson();
2660
+ const { result, next } = fn(current);
2661
+ if (next !== void 0) {
2662
+ this.writeAuthJson(next);
2663
+ }
2664
+ return result;
2665
+ }
2666
+ async withLockAsync(fn) {
2667
+ const current = this.readAuthJson();
2668
+ const { result, next } = await fn(current);
2669
+ if (next !== void 0) {
2670
+ this.writeAuthJson(next);
2671
+ }
2672
+ return result;
2673
+ }
2674
+ };
2675
+
2676
+ // src/runtime/agent.ts
2677
+ init_attachment_utils();
2678
+
2679
+ // src/runtime/artifacts/query-service.ts
2680
+ function clampLimit(limit, fallback, max) {
2681
+ if (!Number.isFinite(limit)) {
2682
+ return fallback;
2683
+ }
2684
+ const normalized = Math.floor(limit);
2685
+ return Math.max(1, Math.min(normalized, max));
2686
+ }
2687
+ function clampOffset(offset) {
2688
+ if (!Number.isFinite(offset)) {
2689
+ return 0;
2690
+ }
2691
+ return Math.max(0, Math.floor(offset));
2692
+ }
2693
+ var ResultsQueryService = class {
2694
+ constructor(resultStore) {
2695
+ this.resultStore = resultStore;
2696
+ }
2697
+ async listRecentArtifacts(options = {}) {
2698
+ return this.resultStore.listRecentArtifacts({
2699
+ channelId: options.channelId,
2700
+ limit: clampLimit(options.limit, 100, 500),
2701
+ offset: clampOffset(options.offset)
2702
+ });
2703
+ }
2704
+ };
2705
+
2706
+ // src/runtime/artifacts/snapshot-service.ts
2707
+ import { randomUUID } from "crypto";
2708
+ import fs9 from "fs";
2709
+ import path9 from "path";
2710
+
2711
+ // src/runtime/files/metadata.ts
2712
+ import fs8 from "fs";
2713
+ import path8 from "path";
2514
2714
  var MIME_BY_EXT = {
2515
2715
  ".png": "image/png",
2516
2716
  ".jpg": "image/jpeg",
@@ -2532,26 +2732,447 @@ var MIME_BY_EXT = {
2532
2732
  ".ogg": "audio/ogg"
2533
2733
  };
2534
2734
  function detectMimeType(filePath) {
2535
- const ext = path7.extname(filePath).toLowerCase();
2735
+ const ext = path8.extname(filePath).toLowerCase();
2536
2736
  return MIME_BY_EXT[ext];
2537
2737
  }
2538
- function createSendFileTool(fileOutputCallbackRef) {
2738
+ function isWithinDirectory(parentDir, targetPath) {
2739
+ const relativePath = path8.relative(path8.resolve(parentDir), path8.resolve(targetPath));
2740
+ return relativePath !== ".." && !relativePath.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relativePath);
2741
+ }
2742
+ function toPackRelativePath(rootDir, filePath) {
2743
+ const resolvedRoot = path8.resolve(rootDir);
2744
+ const resolvedFile = path8.resolve(filePath);
2745
+ if (!isWithinDirectory(resolvedRoot, resolvedFile)) {
2746
+ throw new Error(`Path is outside the pack root: ${resolvedFile}`);
2747
+ }
2748
+ return path8.relative(resolvedRoot, resolvedFile).split(path8.sep).join("/");
2749
+ }
2750
+ function resolvePackFile(rootDir, filePath) {
2751
+ if (!path8.isAbsolute(filePath)) {
2752
+ throw new Error(`filePath must be absolute: ${filePath}`);
2753
+ }
2754
+ const resolvedPath = path8.resolve(filePath);
2755
+ if (!isWithinDirectory(rootDir, resolvedPath)) {
2756
+ throw new Error(`File is outside the pack root: ${resolvedPath}`);
2757
+ }
2758
+ if (!fs8.existsSync(resolvedPath)) {
2759
+ throw new Error(`File not found: ${resolvedPath}`);
2760
+ }
2761
+ const stats = fs8.statSync(resolvedPath);
2762
+ if (!stats.isFile()) {
2763
+ throw new Error(`Path is not a file: ${resolvedPath}`);
2764
+ }
2765
+ fs8.accessSync(resolvedPath, fs8.constants.R_OK);
2539
2766
  return {
2540
- name: "send_file",
2541
- label: "Send File",
2542
- description: "Send a file to the user via the current chat channel (Telegram, Slack, or Web). IMPORTANT: Do NOT proactively send files. Only use this tool when the user EXPLICITLY asks you to send, share, or deliver a file (e.g. '\u628A\u6587\u4EF6\u53D1\u7ED9\u6211', 'send me the file', 'share the result'). Never send intermediate/temporary files. When the user asks, send only the specific file(s) the user requested, not all generated files.",
2543
- promptSnippet: "send_file: Send a file to the user ONLY when they explicitly request it. Never send files proactively or automatically.",
2544
- parameters: SendFileParams,
2545
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
2546
- const { filePath, caption } = params;
2547
- if (!fs7.existsSync(filePath)) {
2548
- return {
2549
- content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
2550
- details: void 0
2551
- };
2552
- }
2553
- const stats = fs7.statSync(filePath);
2554
- if (!stats.isFile()) {
2767
+ resolvedPath,
2768
+ fileName: path8.basename(resolvedPath),
2769
+ mimeType: detectMimeType(resolvedPath),
2770
+ sizeBytes: stats.size
2771
+ };
2772
+ }
2773
+
2774
+ // src/runtime/artifacts/snapshot-service.ts
2775
+ function sanitizeFileName(fileName) {
2776
+ const sanitized = fileName.replace(/[^a-zA-Z0-9._-]+/g, "-");
2777
+ return sanitized || "artifact";
2778
+ }
2779
+ function formatSnapshotStamp(isoDate) {
2780
+ const normalized = isoDate.replace(/\D+/g, "").slice(0, 14);
2781
+ return normalized || String(Date.now());
2782
+ }
2783
+ var ArtifactSnapshotService = class {
2784
+ constructor(rootDir) {
2785
+ this.rootDir = rootDir;
2786
+ }
2787
+ createSnapshots(runId, artifacts, declaredAt) {
2788
+ if (artifacts.length === 0) {
2789
+ return [];
2790
+ }
2791
+ const artifactsRoot = path9.resolve(this.rootDir, "data", "artifacts");
2792
+ const runDir = path9.join(artifactsRoot, runId);
2793
+ const tempDir = path9.join(
2794
+ artifactsRoot,
2795
+ `.tmp-${runId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
2796
+ );
2797
+ const snapshots = [];
2798
+ const movedPaths = [];
2799
+ const snapshotNames = artifacts.map((artifact) => [
2800
+ formatSnapshotStamp(declaredAt),
2801
+ randomUUID(),
2802
+ sanitizeFileName(artifact.fileName)
2803
+ ].join("-"));
2804
+ fs9.mkdirSync(artifactsRoot, { recursive: true });
2805
+ fs9.mkdirSync(tempDir, { recursive: true });
2806
+ try {
2807
+ snapshotNames.forEach((snapshotName, index) => {
2808
+ fs9.copyFileSync(
2809
+ artifacts[index].filePath,
2810
+ path9.join(tempDir, snapshotName)
2811
+ );
2812
+ });
2813
+ fs9.mkdirSync(runDir, { recursive: true });
2814
+ snapshotNames.forEach((snapshotName, index) => {
2815
+ const artifact = artifacts[index];
2816
+ const tempSnapshotPath = path9.join(tempDir, snapshotName);
2817
+ const finalSnapshotPath = path9.join(runDir, snapshotName);
2818
+ fs9.renameSync(tempSnapshotPath, finalSnapshotPath);
2819
+ movedPaths.push(finalSnapshotPath);
2820
+ snapshots.push({
2821
+ declaredAt,
2822
+ originalPath: toPackRelativePath(this.rootDir, artifact.filePath),
2823
+ snapshotPath: path9.join("data", "artifacts", runId, snapshotName).split(path9.sep).join("/"),
2824
+ fileName: artifact.fileName,
2825
+ mimeType: artifact.mimeType,
2826
+ sizeBytes: artifact.sizeBytes,
2827
+ title: artifact.title,
2828
+ isPrimary: artifact.isPrimary
2829
+ });
2830
+ });
2831
+ fs9.rmSync(tempDir, { recursive: true, force: true });
2832
+ return snapshots;
2833
+ } catch (error) {
2834
+ fs9.rmSync(tempDir, { recursive: true, force: true });
2835
+ movedPaths.forEach((filePath) => fs9.rmSync(filePath, { force: true }));
2836
+ this.removeEmptyRunDirectory(runDir);
2837
+ throw error;
2838
+ }
2839
+ }
2840
+ removeSnapshots(snapshotPaths) {
2841
+ const visitedRunDirs = /* @__PURE__ */ new Set();
2842
+ for (const snapshotPath of snapshotPaths) {
2843
+ const resolvedPath = path9.resolve(this.rootDir, snapshotPath);
2844
+ fs9.rmSync(resolvedPath, { force: true });
2845
+ visitedRunDirs.add(path9.dirname(resolvedPath));
2846
+ }
2847
+ visitedRunDirs.forEach((runDir) => this.removeEmptyRunDirectory(runDir));
2848
+ }
2849
+ removeEmptyRunDirectory(runDir) {
2850
+ try {
2851
+ if (!fs9.existsSync(runDir)) {
2852
+ return;
2853
+ }
2854
+ if (fs9.readdirSync(runDir).length === 0) {
2855
+ fs9.rmdirSync(runDir);
2856
+ }
2857
+ } catch {
2858
+ }
2859
+ }
2860
+ };
2861
+
2862
+ // src/runtime/artifacts/persistence-service.ts
2863
+ var ArtifactPersistenceService = class {
2864
+ constructor(snapshotService, resultStore) {
2865
+ this.snapshotService = snapshotService;
2866
+ this.resultStore = resultStore;
2867
+ }
2868
+ async saveArtifacts(input) {
2869
+ const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
2870
+ const snapshots = this.snapshotService.createSnapshots(
2871
+ input.runId,
2872
+ input.artifacts,
2873
+ declaredAt
2874
+ );
2875
+ try {
2876
+ await this.resultStore.insertArtifacts({
2877
+ runId: input.runId,
2878
+ channelId: input.channelId,
2879
+ artifacts: snapshots
2880
+ });
2881
+ return snapshots.length;
2882
+ } catch (error) {
2883
+ this.snapshotService.removeSnapshots(
2884
+ snapshots.map((artifact) => artifact.snapshotPath)
2885
+ );
2886
+ throw error;
2887
+ }
2888
+ }
2889
+ };
2890
+
2891
+ // src/runtime/artifacts/store.ts
2892
+ import fs10 from "fs";
2893
+ import path10 from "path";
2894
+ import { randomUUID as randomUUID2 } from "crypto";
2895
+ import sqlite3 from "sqlite3";
2896
+ function mapArtifactRow(row) {
2897
+ return {
2898
+ artifactId: row.artifact_id,
2899
+ runId: row.run_id,
2900
+ channelId: row.channel_id,
2901
+ originalPath: row.original_path,
2902
+ snapshotPath: row.snapshot_path,
2903
+ fileName: row.file_name,
2904
+ mimeType: row.mime_type,
2905
+ sizeBytes: row.size_bytes,
2906
+ title: row.title,
2907
+ isPrimary: row.is_primary === 1,
2908
+ declaredAt: row.declared_at
2909
+ };
2910
+ }
2911
+ var ResultStore = class {
2912
+ db = null;
2913
+ ready;
2914
+ constructor(rootDir) {
2915
+ const dataDir = path10.resolve(rootDir, "data");
2916
+ fs10.mkdirSync(dataDir, { recursive: true });
2917
+ this.ready = this.initialize(path10.join(dataDir, "result-v2.db"));
2918
+ }
2919
+ async initialize(databasePath) {
2920
+ this.db = await openDatabase(databasePath);
2921
+ await this.exec("PRAGMA journal_mode = WAL");
2922
+ await this.exec(`
2923
+ CREATE TABLE IF NOT EXISTS artifacts (
2924
+ artifact_id TEXT PRIMARY KEY,
2925
+ run_id TEXT NOT NULL,
2926
+ channel_id TEXT NOT NULL,
2927
+ original_path TEXT NOT NULL,
2928
+ snapshot_path TEXT NOT NULL,
2929
+ file_name TEXT NOT NULL,
2930
+ mime_type TEXT,
2931
+ size_bytes INTEGER NOT NULL,
2932
+ title TEXT,
2933
+ is_primary INTEGER NOT NULL DEFAULT 0,
2934
+ declared_at TEXT NOT NULL
2935
+ );
2936
+
2937
+ CREATE INDEX IF NOT EXISTS idx_artifacts_channel_declared_at
2938
+ ON artifacts(channel_id, declared_at DESC);
2939
+ `);
2940
+ }
2941
+ async insertArtifacts(input) {
2942
+ await this.ready;
2943
+ if (input.artifacts.length === 0) {
2944
+ return;
2945
+ }
2946
+ const insertArtifact = `
2947
+ INSERT INTO artifacts (
2948
+ artifact_id,
2949
+ run_id,
2950
+ channel_id,
2951
+ original_path,
2952
+ snapshot_path,
2953
+ file_name,
2954
+ mime_type,
2955
+ size_bytes,
2956
+ title,
2957
+ is_primary,
2958
+ declared_at
2959
+ ) VALUES (
2960
+ ?,
2961
+ ?,
2962
+ ?,
2963
+ ?,
2964
+ ?,
2965
+ ?,
2966
+ ?,
2967
+ ?,
2968
+ ?,
2969
+ ?,
2970
+ ?
2971
+ )
2972
+ `;
2973
+ await this.exec("BEGIN");
2974
+ try {
2975
+ for (const artifact of input.artifacts) {
2976
+ await this.run(insertArtifact, [
2977
+ randomUUID2(),
2978
+ input.runId,
2979
+ input.channelId,
2980
+ artifact.originalPath,
2981
+ artifact.snapshotPath,
2982
+ artifact.fileName,
2983
+ artifact.mimeType ?? null,
2984
+ artifact.sizeBytes,
2985
+ artifact.title ?? null,
2986
+ artifact.isPrimary ? 1 : 0,
2987
+ artifact.declaredAt
2988
+ ]);
2989
+ }
2990
+ await this.exec("COMMIT");
2991
+ } catch (error) {
2992
+ await this.rollback();
2993
+ throw error;
2994
+ }
2995
+ }
2996
+ async listRecentArtifacts(options = {}) {
2997
+ await this.ready;
2998
+ const limit = options.limit ?? 100;
2999
+ const offset = options.offset ?? 0;
3000
+ const conditions = [];
3001
+ const params = [];
3002
+ if (options.channelId) {
3003
+ conditions.push("channel_id = ?");
3004
+ params.push(options.channelId);
3005
+ }
3006
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3007
+ const rows = await this.all(`
3008
+ SELECT *
3009
+ FROM artifacts
3010
+ ${whereClause}
3011
+ ORDER BY declared_at DESC, rowid DESC
3012
+ LIMIT ? OFFSET ?
3013
+ `, [...params, limit, offset]);
3014
+ return rows.map(mapArtifactRow);
3015
+ }
3016
+ getDatabase() {
3017
+ if (!this.db) {
3018
+ throw new Error("Result store database is not ready");
3019
+ }
3020
+ return this.db;
3021
+ }
3022
+ exec(sql) {
3023
+ const db = this.getDatabase();
3024
+ return new Promise((resolve, reject) => {
3025
+ db.exec(sql, (error) => {
3026
+ if (error) {
3027
+ reject(error);
3028
+ return;
3029
+ }
3030
+ resolve();
3031
+ });
3032
+ });
3033
+ }
3034
+ run(sql, params = []) {
3035
+ const db = this.getDatabase();
3036
+ return new Promise((resolve, reject) => {
3037
+ db.run(sql, params, (error) => {
3038
+ if (error) {
3039
+ reject(error);
3040
+ return;
3041
+ }
3042
+ resolve();
3043
+ });
3044
+ });
3045
+ }
3046
+ all(sql, params = []) {
3047
+ const db = this.getDatabase();
3048
+ return new Promise((resolve, reject) => {
3049
+ db.all(sql, params, (error, rows) => {
3050
+ if (error) {
3051
+ reject(error);
3052
+ return;
3053
+ }
3054
+ resolve(rows);
3055
+ });
3056
+ });
3057
+ }
3058
+ async rollback() {
3059
+ try {
3060
+ await this.exec("ROLLBACK");
3061
+ } catch {
3062
+ }
3063
+ }
3064
+ };
3065
+ function openDatabase(databasePath) {
3066
+ return new Promise((resolve, reject) => {
3067
+ const db = new sqlite3.Database(databasePath, (error) => {
3068
+ if (error) {
3069
+ reject(error);
3070
+ return;
3071
+ }
3072
+ resolve(db);
3073
+ });
3074
+ });
3075
+ }
3076
+
3077
+ // src/runtime/artifacts/save-artifacts-tool.ts
3078
+ import { Type } from "@sinclair/typebox";
3079
+ var ArtifactItem = Type.Object({
3080
+ filePath: Type.String({
3081
+ description: "Absolute path to the artifact file. The file must exist, be readable, and be inside the current pack root."
3082
+ }),
3083
+ title: Type.Optional(
3084
+ Type.String({
3085
+ description: "Optional short title shown in the dashboard."
3086
+ })
3087
+ ),
3088
+ isPrimary: Type.Optional(
3089
+ Type.Boolean({
3090
+ description: "Mark this artifact as a primary output."
3091
+ })
3092
+ )
3093
+ });
3094
+ var SaveArtifactsParams = Type.Object({
3095
+ artifacts: Type.Array(ArtifactItem, {
3096
+ minItems: 1,
3097
+ description: "The artifact files to save for this run."
3098
+ })
3099
+ });
3100
+ function textResult(text) {
3101
+ return { content: [{ type: "text", text }], details: void 0 };
3102
+ }
3103
+ function normalizeOptionalText(value) {
3104
+ const trimmed = value?.trim();
3105
+ return trimmed ? trimmed : void 0;
3106
+ }
3107
+ function createSaveArtifactsTool(rootDir, saveCallbackRef) {
3108
+ return {
3109
+ name: "save_artifacts",
3110
+ label: "Save Artifacts",
3111
+ description: [
3112
+ "Save the final output files produced by this run.",
3113
+ "Always use this for user-facing deliverables that are part of the final result.",
3114
+ "Do not use this for intermediate, temporary, draft, or scratch files.",
3115
+ "Each filePath must be an absolute path inside the current pack root."
3116
+ ].join("\n"),
3117
+ promptSnippet: "save_artifacts: Save final result files for this task. Always call this for user-facing final deliverables, and never for intermediate files. Use absolute paths inside the current pack root.",
3118
+ promptGuidelines: [
3119
+ "Whenever you create a final result file for the user, call `save_artifacts` before finishing the response.",
3120
+ "Do not call `save_artifacts` for intermediate, temporary, draft, or scratch files."
3121
+ ],
3122
+ parameters: SaveArtifactsParams,
3123
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
3124
+ const saveArtifacts = saveCallbackRef.current;
3125
+ if (!saveArtifacts) {
3126
+ throw new Error("Artifact saving is not available for this run.");
3127
+ }
3128
+ const artifacts = params.artifacts.map((artifact) => {
3129
+ const metadata = resolvePackFile(rootDir, artifact.filePath);
3130
+ return {
3131
+ filePath: metadata.resolvedPath,
3132
+ fileName: metadata.fileName,
3133
+ mimeType: metadata.mimeType,
3134
+ sizeBytes: metadata.sizeBytes,
3135
+ title: normalizeOptionalText(artifact.title),
3136
+ isPrimary: artifact.isPrimary === true
3137
+ };
3138
+ });
3139
+ const savedCount = await saveArtifacts(artifacts);
3140
+ return textResult(`Saved ${savedCount} artifact(s).`);
3141
+ }
3142
+ };
3143
+ }
3144
+
3145
+ // src/runtime/tools/send-file-tool.ts
3146
+ import fs11 from "fs";
3147
+ import path11 from "path";
3148
+ import { Type as Type2 } from "@sinclair/typebox";
3149
+ var SendFileParams = Type2.Object({
3150
+ filePath: Type2.String({
3151
+ description: "Absolute path to the file to send to the user. The file must exist and be readable."
3152
+ }),
3153
+ caption: Type2.Optional(
3154
+ Type2.String({
3155
+ description: "Optional caption or description to accompany the file."
3156
+ })
3157
+ )
3158
+ });
3159
+ function createSendFileTool(fileOutputCallbackRef) {
3160
+ return {
3161
+ name: "send_file",
3162
+ label: "Send File",
3163
+ description: "Send a file to the user via the current chat channel (Telegram, Slack, or Web). IMPORTANT: Do NOT proactively send files. Only use this tool when the user EXPLICITLY asks you to send, share, or deliver a file (e.g. '\u628A\u6587\u4EF6\u53D1\u7ED9\u6211', 'send me the file', 'share the result'). Never send intermediate/temporary files. When the user asks, send only the specific file(s) the user requested, not all generated files.",
3164
+ promptSnippet: "send_file: Send a file to the user ONLY when they explicitly request it. Never send files proactively or automatically.",
3165
+ parameters: SendFileParams,
3166
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
3167
+ const { filePath, caption } = params;
3168
+ if (!fs11.existsSync(filePath)) {
3169
+ return {
3170
+ content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
3171
+ details: void 0
3172
+ };
3173
+ }
3174
+ const stats = fs11.statSync(filePath);
3175
+ if (!stats.isFile()) {
2555
3176
  return {
2556
3177
  content: [
2557
3178
  { type: "text", text: `Error: Path is not a file: ${filePath}` }
@@ -2559,7 +3180,7 @@ function createSendFileTool(fileOutputCallbackRef) {
2559
3180
  details: void 0
2560
3181
  };
2561
3182
  }
2562
- const filename = path7.basename(filePath);
3183
+ const filename = path11.basename(filePath);
2563
3184
  const mimeType = detectMimeType(filePath);
2564
3185
  const callback = fileOutputCallbackRef.current;
2565
3186
  if (callback) {
@@ -2586,43 +3207,65 @@ function createSendFileTool(fileOutputCallbackRef) {
2586
3207
  }
2587
3208
 
2588
3209
  // src/runtime/tools/manage-schedule-tool.ts
2589
- import { Type as Type2 } from "@sinclair/typebox";
2590
- var ManageScheduleParams = Type2.Object({
2591
- action: Type2.Union(
3210
+ import { Type as Type3 } from "@sinclair/typebox";
3211
+ var ManageScheduleParams = Type3.Object({
3212
+ action: Type3.Union(
2592
3213
  [
2593
- Type2.Literal("add"),
2594
- Type2.Literal("list"),
2595
- Type2.Literal("remove"),
2596
- Type2.Literal("trigger"),
2597
- Type2.Literal("enable"),
2598
- Type2.Literal("disable")
3214
+ Type3.Literal("add"),
3215
+ Type3.Literal("list"),
3216
+ Type3.Literal("remove"),
3217
+ Type3.Literal("trigger"),
3218
+ Type3.Literal("enable"),
3219
+ Type3.Literal("disable")
2599
3220
  ],
2600
3221
  { description: "The action to perform." }
2601
3222
  ),
2602
- name: Type2.Optional(
2603
- Type2.String({
3223
+ name: Type3.Optional(
3224
+ Type3.String({
2604
3225
  description: "Unique name for the scheduled task. Required for add/remove/trigger/enable/disable."
2605
3226
  })
2606
3227
  ),
2607
- cron: Type2.Optional(
2608
- Type2.String({
3228
+ cron: Type3.Optional(
3229
+ Type3.String({
2609
3230
  description: "Cron expression (5 fields: minute hour day month weekday). Required for add."
2610
3231
  })
2611
3232
  ),
2612
- prompt: Type2.Optional(
2613
- Type2.String({
3233
+ prompt: Type3.Optional(
3234
+ Type3.String({
2614
3235
  description: "The work prompt to execute when the task triggers. Required for add. Describe only what to do each run; do not repeat timing, cron, or 'every N minutes' instructions here."
2615
3236
  })
2616
3237
  ),
2617
- timezone: Type2.Optional(
2618
- Type2.String({
3238
+ timezone: Type3.Optional(
3239
+ Type3.String({
2619
3240
  description: "Optional timezone for the cron schedule, e.g. 'Asia/Shanghai', 'America/New_York'."
2620
3241
  })
3242
+ ),
3243
+ notifyAdapter: Type3.Optional(
3244
+ Type3.String({
3245
+ description: "Optional target adapter for notifications. If omitted, the current chat is used when supported (Telegram, Slack, or Web)."
3246
+ })
3247
+ ),
3248
+ notifyChannelId: Type3.Optional(
3249
+ Type3.String({
3250
+ description: "Optional target channelId for notifications. Must be provided together with notifyAdapter when overriding the default target."
3251
+ })
2621
3252
  )
2622
3253
  });
2623
- function textResult(text) {
3254
+ function textResult2(text) {
2624
3255
  return { content: [{ type: "text", text }], details: void 0 };
2625
3256
  }
3257
+ function getDefaultNotifyTarget(adapter, channelId) {
3258
+ if (adapter === "telegram" && channelId.startsWith("telegram-")) {
3259
+ return { adapter: "telegram", channelId };
3260
+ }
3261
+ if (adapter === "slack" && channelId.startsWith("slack-")) {
3262
+ return { adapter: "slack", channelId };
3263
+ }
3264
+ if (adapter === "web") {
3265
+ return { adapter: "web", channelId };
3266
+ }
3267
+ return null;
3268
+ }
2626
3269
  function createManageScheduleTool(schedulerRef, adapter, channelId) {
2627
3270
  return {
2628
3271
  name: "manage_scheduled_task",
@@ -2631,7 +3274,7 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
2631
3274
  "Manage scheduled tasks (cron jobs) that automatically execute prompts and push results to IM channels.",
2632
3275
  "",
2633
3276
  "Actions:",
2634
- "- add: Create a new scheduled task. Requires: name, cron, prompt. The notification target always uses the current Telegram or Slack chat. The prompt must describe only the work for each run, not the schedule itself.",
3277
+ "- add: Create a new scheduled task. Requires: name, cron, prompt. Notifications default to the current Telegram, Slack, or Web chat. You can override the destination with notifyAdapter + notifyChannelId. The prompt must describe only the work for each run, not the schedule itself.",
2635
3278
  "- list: List all scheduled tasks with their status.",
2636
3279
  "- remove: Remove a scheduled task by name.",
2637
3280
  "- trigger: Manually trigger a scheduled task by name (runs immediately).",
@@ -2648,7 +3291,7 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
2648
3291
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
2649
3292
  const scheduler = schedulerRef.current;
2650
3293
  if (!scheduler) {
2651
- return textResult(
3294
+ return textResult2(
2652
3295
  "Error: Scheduler is not available. The scheduled task system may not be initialized."
2653
3296
  );
2654
3297
  }
@@ -2656,79 +3299,85 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
2656
3299
  case "list": {
2657
3300
  const jobs = scheduler.listJobs();
2658
3301
  if (jobs.length === 0) {
2659
- return textResult("No scheduled tasks configured.");
3302
+ return textResult2("No scheduled tasks configured.");
2660
3303
  }
2661
3304
  const lines = jobs.map(
2662
3305
  (j) => `- **${j.name}**: \`${j.cron}\` \u2192 ${j.notify.adapter}:${j.notify.channelId} [${j.enabled ? "enabled" : "disabled"}]${j.running ? " (running)" : ""}${j.lastRunAt ? ` (last: ${j.lastRunAt})` : ""}`
2663
3306
  );
2664
- return textResult(
3307
+ return textResult2(
2665
3308
  `Scheduled tasks (${jobs.length}):
2666
3309
  ${lines.join("\n")}`
2667
3310
  );
2668
3311
  }
2669
3312
  case "add": {
2670
3313
  if (!params.name || !params.cron || !params.prompt) {
2671
- return textResult(
3314
+ return textResult2(
2672
3315
  "Error: 'name', 'cron', and 'prompt' are required for adding a task."
2673
3316
  );
2674
3317
  }
2675
- if (adapter !== "telegram" && adapter !== "slack") {
2676
- return textResult(
2677
- "Error: Scheduled tasks can only be created from a Telegram or Slack."
3318
+ if (params.notifyAdapter && !params.notifyChannelId || !params.notifyAdapter && params.notifyChannelId) {
3319
+ return textResult2(
3320
+ "Error: 'notifyAdapter' and 'notifyChannelId' must be provided together when overriding the notification target."
3321
+ );
3322
+ }
3323
+ const notify = params.notifyAdapter && params.notifyChannelId ? {
3324
+ adapter: params.notifyAdapter,
3325
+ channelId: params.notifyChannelId
3326
+ } : getDefaultNotifyTarget(adapter, channelId);
3327
+ if (!notify) {
3328
+ return textResult2(
3329
+ "Error: No default notification target is available for this chat. Provide 'notifyAdapter' and 'notifyChannelId'."
2678
3330
  );
2679
3331
  }
2680
3332
  const jobConfig = {
2681
3333
  name: params.name,
2682
3334
  cron: params.cron,
2683
3335
  prompt: params.prompt,
2684
- notify: {
2685
- adapter,
2686
- channelId
2687
- },
3336
+ notify,
2688
3337
  enabled: true,
2689
3338
  timezone: params.timezone
2690
3339
  };
2691
3340
  const result = scheduler.addJob(jobConfig);
2692
- return textResult(result.message);
3341
+ return textResult2(result.message);
2693
3342
  }
2694
3343
  case "remove": {
2695
3344
  if (!params.name) {
2696
- return textResult(
3345
+ return textResult2(
2697
3346
  "Error: 'name' is required for removing a task."
2698
3347
  );
2699
3348
  }
2700
3349
  const result = scheduler.removeJob(params.name);
2701
- return textResult(result.message);
3350
+ return textResult2(result.message);
2702
3351
  }
2703
3352
  case "trigger": {
2704
3353
  if (!params.name) {
2705
- return textResult(
3354
+ return textResult2(
2706
3355
  "Error: 'name' is required for triggering a task."
2707
3356
  );
2708
3357
  }
2709
3358
  const result = await scheduler.triggerJob(params.name);
2710
- return textResult(result.message);
3359
+ return textResult2(result.message);
2711
3360
  }
2712
3361
  case "enable": {
2713
3362
  if (!params.name) {
2714
- return textResult(
3363
+ return textResult2(
2715
3364
  "Error: 'name' is required for enabling a task."
2716
3365
  );
2717
3366
  }
2718
3367
  const result = scheduler.setEnabled(params.name, true);
2719
- return textResult(result.message);
3368
+ return textResult2(result.message);
2720
3369
  }
2721
3370
  case "disable": {
2722
3371
  if (!params.name) {
2723
- return textResult(
3372
+ return textResult2(
2724
3373
  "Error: 'name' is required for disabling a task."
2725
3374
  );
2726
3375
  }
2727
3376
  const result = scheduler.setEnabled(params.name, false);
2728
- return textResult(result.message);
3377
+ return textResult2(result.message);
2729
3378
  }
2730
3379
  default:
2731
- return textResult(
3380
+ return textResult2(
2732
3381
  `Error: Unknown action '${params.action}'. Use: add, list, remove, trigger, enable, disable.`
2733
3382
  );
2734
3383
  }
@@ -2738,8 +3387,8 @@ ${lines.join("\n")}`
2738
3387
 
2739
3388
  // src/runtime/commands/help-command.ts
2740
3389
  init_commands();
2741
- import fs8 from "fs";
2742
- import path8 from "path";
3390
+ import fs12 from "fs";
3391
+ import path12 from "path";
2743
3392
  function buildHelpMessage(rootDir) {
2744
3393
  const sections = [];
2745
3394
  const commands = getVisibleCommands();
@@ -2749,7 +3398,7 @@ function buildHelpMessage(rootDir) {
2749
3398
  sections.push(`\u{1F4CB} **Available Commands**
2750
3399
 
2751
3400
  ${commandLines.join("\n")}`);
2752
- const configPath = path8.resolve(rootDir, "skillpack.json");
3401
+ const configPath = path12.resolve(rootDir, "skillpack.json");
2753
3402
  const skills = readInstalledSkills(configPath);
2754
3403
  if (skills.length > 0) {
2755
3404
  const skillLines = skills.map(
@@ -2785,11 +3434,11 @@ function handleHelpCommand(rootDir) {
2785
3434
  };
2786
3435
  }
2787
3436
  function readInstalledSkills(configPath) {
2788
- if (!fs8.existsSync(configPath)) {
3437
+ if (!fs12.existsSync(configPath)) {
2789
3438
  return [];
2790
3439
  }
2791
3440
  try {
2792
- const raw = fs8.readFileSync(configPath, "utf-8");
3441
+ const raw = fs12.readFileSync(configPath, "utf-8");
2793
3442
  const config = JSON.parse(raw);
2794
3443
  return Array.isArray(config.skills) ? config.skills : [];
2795
3444
  } catch {
@@ -2809,24 +3458,24 @@ var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
2809
3458
  var PACK_AGENTS_FILE = "AGENTS.md";
2810
3459
  var PACK_SOUL_FILE = "SOUL.md";
2811
3460
  function materializeBuiltinSkillCreator(rootDir, skillsPath) {
2812
- if (!fs9.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
3461
+ if (!fs13.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
2813
3462
  log(
2814
3463
  `[PackAgent] Built-in skill-creator template missing: ${BUILTIN_SKILL_CREATOR_TEMPLATE_DIR}`
2815
3464
  );
2816
3465
  return null;
2817
3466
  }
2818
- const packConfigPath = path9.resolve(rootDir, "skillpack.json");
2819
- const skillDir = path9.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
2820
- const skillPath = path9.join(skillDir, "SKILL.md");
3467
+ const packConfigPath = path13.resolve(rootDir, "skillpack.json");
3468
+ const skillDir = path13.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
3469
+ const skillPath = path13.join(skillDir, "SKILL.md");
2821
3470
  const renderTemplate = (content) => content.replaceAll("{{SKILLS_PATH}}", skillsPath).replaceAll("{{PACK_CONFIG_PATH}}", packConfigPath);
2822
3471
  const copyDir = (srcDir, destDir) => {
2823
- fs9.mkdirSync(destDir, { recursive: true });
2824
- for (const entry of fs9.readdirSync(srcDir, { withFileTypes: true })) {
3472
+ fs13.mkdirSync(destDir, { recursive: true });
3473
+ for (const entry of fs13.readdirSync(srcDir, { withFileTypes: true })) {
2825
3474
  if (entry.name === ".DS_Store") {
2826
3475
  continue;
2827
3476
  }
2828
- const srcPath = path9.join(srcDir, entry.name);
2829
- const destPath = path9.join(destDir, entry.name);
3477
+ const srcPath = path13.join(srcDir, entry.name);
3478
+ const destPath = path13.join(destDir, entry.name);
2830
3479
  if (entry.isDirectory()) {
2831
3480
  copyDir(srcPath, destPath);
2832
3481
  continue;
@@ -2835,17 +3484,17 @@ function materializeBuiltinSkillCreator(rootDir, skillsPath) {
2835
3484
  continue;
2836
3485
  }
2837
3486
  if (entry.name.endsWith(".md") || entry.name.endsWith(".py")) {
2838
- const content = fs9.readFileSync(srcPath, "utf-8");
2839
- fs9.writeFileSync(destPath, renderTemplate(content), "utf-8");
3487
+ const content = fs13.readFileSync(srcPath, "utf-8");
3488
+ fs13.writeFileSync(destPath, renderTemplate(content), "utf-8");
2840
3489
  continue;
2841
3490
  }
2842
- fs9.copyFileSync(srcPath, destPath);
3491
+ fs13.copyFileSync(srcPath, destPath);
2843
3492
  }
2844
3493
  };
2845
- if (!fs9.existsSync(skillDir)) {
3494
+ if (!fs13.existsSync(skillDir)) {
2846
3495
  copyDir(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR, skillDir);
2847
3496
  }
2848
- if (!fs9.existsSync(skillPath)) {
3497
+ if (!fs13.existsSync(skillPath)) {
2849
3498
  log(
2850
3499
  `[PackAgent] Materialized built-in skill-creator but SKILL.md is missing: ${skillPath}`
2851
3500
  );
@@ -2873,11 +3522,11 @@ function overrideBuiltinSkillCreator(base, materializedSkill) {
2873
3522
  };
2874
3523
  }
2875
3524
  function readOptionalPackPromptFile(filePath) {
2876
- if (!fs9.existsSync(filePath)) {
3525
+ if (!fs13.existsSync(filePath)) {
2877
3526
  return void 0;
2878
3527
  }
2879
3528
  try {
2880
- const content = fs9.readFileSync(filePath, "utf-8").trim();
3529
+ const content = fs13.readFileSync(filePath, "utf-8").trim();
2881
3530
  return content.length > 0 ? content : void 0;
2882
3531
  } catch (error) {
2883
3532
  console.warn(`[PackAgent] Warning: Could not read ${filePath}:`, error);
@@ -2885,8 +3534,8 @@ function readOptionalPackPromptFile(filePath) {
2885
3534
  }
2886
3535
  }
2887
3536
  function buildPackPromptBlock(rootDir) {
2888
- const agentsPath = path9.resolve(rootDir, PACK_AGENTS_FILE);
2889
- const soulPath = path9.resolve(rootDir, PACK_SOUL_FILE);
3537
+ const agentsPath = path13.resolve(rootDir, PACK_AGENTS_FILE);
3538
+ const soulPath = path13.resolve(rootDir, PACK_SOUL_FILE);
2890
3539
  const agentsContent = readOptionalPackPromptFile(agentsPath);
2891
3540
  const soulContent = readOptionalPackPromptFile(soulPath);
2892
3541
  if (!agentsContent && !soulContent) {
@@ -2945,14 +3594,13 @@ var PackAgent = class {
2945
3594
  options;
2946
3595
  channels = /* @__PURE__ */ new Map();
2947
3596
  pendingSessionCreations = /* @__PURE__ */ new Map();
2948
- fileOutputCallbackRef = {
2949
- current: null
2950
- };
2951
3597
  schedulerRef = { current: null };
2952
3598
  authStorage;
3599
+ artifactPersistenceService;
2953
3600
  constructor(options) {
2954
3601
  this.options = options;
2955
- const configPath = path9.resolve(options.rootDir, "data", "config.json");
3602
+ this.artifactPersistenceService = options.artifactPersistenceService;
3603
+ const configPath = path13.resolve(options.rootDir, "data", "config.json");
2956
3604
  const backend = new ConfigFileAuthBackend(configPath);
2957
3605
  this.authStorage = AuthStorage.fromStorage(backend);
2958
3606
  const providerMeta = SUPPORTED_PROVIDERS[options.provider];
@@ -2979,9 +3627,12 @@ var PackAgent = class {
2979
3627
  setScheduler(scheduler) {
2980
3628
  this.schedulerRef.current = scheduler;
2981
3629
  }
2982
- createCustomTools(adapter, channelId) {
2983
- const tools = [createSendFileTool(this.fileOutputCallbackRef)];
2984
- if (adapter === "telegram" || adapter === "slack") {
3630
+ createCustomTools(adapter, channelId, fileOutputCallbackRef, finalArtifactsSaveCallbackRef) {
3631
+ const tools = [
3632
+ createSendFileTool(fileOutputCallbackRef),
3633
+ createSaveArtifactsTool(this.options.rootDir, finalArtifactsSaveCallbackRef)
3634
+ ];
3635
+ if (adapter !== "scheduler") {
2985
3636
  tools.push(createManageScheduleTool(this.schedulerRef, adapter, channelId));
2986
3637
  }
2987
3638
  return tools;
@@ -3023,24 +3674,24 @@ var PackAgent = class {
3023
3674
  if (resolvedModel && baseUrl) {
3024
3675
  log(`[PackAgent] Resolved ${provider}/${modelId} api=${resolvedModel.api} baseUrl=${baseUrl}`);
3025
3676
  }
3026
- const sessionDir = path9.resolve(
3677
+ const sessionDir = path13.resolve(
3027
3678
  rootDir,
3028
3679
  "data",
3029
3680
  "sessions",
3030
3681
  channelId
3031
3682
  );
3032
- fs9.mkdirSync(sessionDir, { recursive: true });
3683
+ fs13.mkdirSync(sessionDir, { recursive: true });
3033
3684
  const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
3034
3685
  log(`[PackAgent] Session dir: ${sessionDir}`);
3035
- const workspaceDir = path9.resolve(
3686
+ const workspaceDir = path13.resolve(
3036
3687
  rootDir,
3037
3688
  "data",
3038
3689
  "workspaces",
3039
3690
  channelId
3040
3691
  );
3041
- fs9.mkdirSync(workspaceDir, { recursive: true });
3692
+ fs13.mkdirSync(workspaceDir, { recursive: true });
3042
3693
  log(`[PackAgent] Workspace dir: ${workspaceDir}`);
3043
- const skillsPath = path9.resolve(rootDir, "skills");
3694
+ const skillsPath = path13.resolve(rootDir, "skills");
3044
3695
  log(`[PackAgent] Loading skills from: ${skillsPath}`);
3045
3696
  const materializedSkillCreator = materializeBuiltinSkillCreator(
3046
3697
  rootDir,
@@ -3075,7 +3726,18 @@ var PackAgent = class {
3075
3726
  });
3076
3727
  await resourceLoader.reload();
3077
3728
  const tools = createCodingTools(workspaceDir);
3078
- const customTools = this.createCustomTools(adapter, channelId);
3729
+ const fileOutputCallbackRef = {
3730
+ current: null
3731
+ };
3732
+ const finalArtifactsSaveCallbackRef = {
3733
+ current: null
3734
+ };
3735
+ const customTools = this.createCustomTools(
3736
+ adapter,
3737
+ channelId,
3738
+ fileOutputCallbackRef,
3739
+ finalArtifactsSaveCallbackRef
3740
+ );
3079
3741
  const { session } = await createAgentSession({
3080
3742
  cwd: workspaceDir,
3081
3743
  authStorage,
@@ -3089,7 +3751,9 @@ var PackAgent = class {
3089
3751
  const channelSession = {
3090
3752
  session,
3091
3753
  running: false,
3092
- pending: Promise.resolve()
3754
+ pending: Promise.resolve(),
3755
+ fileOutputCallbackRef,
3756
+ finalArtifactsSaveCallbackRef
3093
3757
  };
3094
3758
  this.channels.set(channelId, channelSession);
3095
3759
  return channelSession;
@@ -3106,86 +3770,97 @@ var PackAgent = class {
3106
3770
  const run = async () => {
3107
3771
  cs.running = true;
3108
3772
  let turnHadVisibleOutput = false;
3109
- this.fileOutputCallbackRef.current = (event) => {
3110
- onEvent(event);
3111
- };
3112
- const unsubscribe = cs.session.subscribe((event) => {
3113
- switch (event.type) {
3114
- case "agent_start":
3115
- log("\n=== [AGENT SESSION START] ===");
3116
- log("System Prompt:\n", cs.session.systemPrompt);
3117
- log("============================\n");
3118
- onEvent({ type: "agent_start" });
3119
- break;
3120
- case "message_start":
3121
- log(`
3773
+ const runId = randomUUID3();
3774
+ let unsubscribe = () => void 0;
3775
+ try {
3776
+ cs.fileOutputCallbackRef.current = (event) => {
3777
+ onEvent(event);
3778
+ };
3779
+ cs.finalArtifactsSaveCallbackRef.current = (artifacts) => {
3780
+ return this.artifactPersistenceService.saveArtifacts({
3781
+ runId,
3782
+ channelId,
3783
+ artifacts
3784
+ });
3785
+ };
3786
+ unsubscribe = cs.session.subscribe((event) => {
3787
+ switch (event.type) {
3788
+ case "agent_start":
3789
+ log("\n=== [AGENT SESSION START] ===");
3790
+ log("System Prompt:\n", cs.session.systemPrompt);
3791
+ log("============================\n");
3792
+ onEvent({ type: "agent_start" });
3793
+ break;
3794
+ case "message_start":
3795
+ log(`
3122
3796
  --- [Message Start: ${event.message?.role}] ---`);
3123
- if (event.message?.role === "user") {
3124
- log(JSON.stringify(event.message.content, null, 2));
3125
- }
3126
- onEvent({ type: "message_start", role: event.message?.role ?? "" });
3127
- break;
3128
- case "message_update":
3129
- if (event.assistantMessageEvent?.type === "text_delta") {
3797
+ if (event.message?.role === "user") {
3798
+ log(JSON.stringify(event.message.content, null, 2));
3799
+ }
3800
+ onEvent({ type: "message_start", role: event.message?.role ?? "" });
3801
+ break;
3802
+ case "message_update":
3803
+ if (event.assistantMessageEvent?.type === "text_delta") {
3804
+ turnHadVisibleOutput = true;
3805
+ write(event.assistantMessageEvent.delta);
3806
+ onEvent({
3807
+ type: "text_delta",
3808
+ delta: event.assistantMessageEvent.delta
3809
+ });
3810
+ } else if (event.assistantMessageEvent?.type === "thinking_delta") {
3811
+ turnHadVisibleOutput = true;
3812
+ onEvent({
3813
+ type: "thinking_delta",
3814
+ delta: event.assistantMessageEvent.delta
3815
+ });
3816
+ }
3817
+ break;
3818
+ case "message_end":
3819
+ log(`
3820
+ --- [Message End: ${event.message?.role}] ---`);
3821
+ if (event.message?.role === "assistant") {
3822
+ const diagnostics2 = getAssistantDiagnostics(event.message);
3823
+ if (diagnostics2) {
3824
+ log(
3825
+ `[Assistant Diagnostics] stopReason=${diagnostics2.stopReason} text=${diagnostics2.hasText ? "yes" : "no"} toolCalls=${diagnostics2.toolCalls}`
3826
+ );
3827
+ if (diagnostics2.errorMessage) {
3828
+ log(`[Assistant Error] ${diagnostics2.errorMessage}`);
3829
+ }
3830
+ }
3831
+ }
3832
+ onEvent({ type: "message_end", role: event.message?.role ?? "" });
3833
+ break;
3834
+ case "tool_execution_start":
3130
3835
  turnHadVisibleOutput = true;
3131
- write(event.assistantMessageEvent.delta);
3836
+ log(`
3837
+ >>> [Tool Start: ${event.toolName}] >>>`);
3838
+ log("Args:", JSON.stringify(event.args, null, 2));
3132
3839
  onEvent({
3133
- type: "text_delta",
3134
- delta: event.assistantMessageEvent.delta
3840
+ type: "tool_start",
3841
+ toolCallId: event.toolCallId ?? "",
3842
+ toolName: event.toolName,
3843
+ toolInput: event.args
3135
3844
  });
3136
- } else if (event.assistantMessageEvent?.type === "thinking_delta") {
3845
+ break;
3846
+ case "tool_execution_end":
3137
3847
  turnHadVisibleOutput = true;
3848
+ log(`<<< [Tool End: ${event.toolName}] <<<`);
3849
+ log(`Error: ${event.isError ? "Yes" : "No"}`);
3138
3850
  onEvent({
3139
- type: "thinking_delta",
3140
- delta: event.assistantMessageEvent.delta
3851
+ type: "tool_end",
3852
+ toolCallId: event.toolCallId ?? "",
3853
+ toolName: event.toolName,
3854
+ isError: event.isError,
3855
+ result: event.result
3141
3856
  });
3142
- }
3143
- break;
3144
- case "message_end":
3145
- log(`
3146
- --- [Message End: ${event.message?.role}] ---`);
3147
- if (event.message?.role === "assistant") {
3148
- const diagnostics = getAssistantDiagnostics(event.message);
3149
- if (diagnostics) {
3150
- log(
3151
- `[Assistant Diagnostics] stopReason=${diagnostics.stopReason} text=${diagnostics.hasText ? "yes" : "no"} toolCalls=${diagnostics.toolCalls}`
3152
- );
3153
- if (diagnostics.errorMessage) {
3154
- log(`[Assistant Error] ${diagnostics.errorMessage}`);
3155
- }
3156
- }
3157
- }
3158
- onEvent({ type: "message_end", role: event.message?.role ?? "" });
3159
- break;
3160
- case "tool_execution_start":
3161
- turnHadVisibleOutput = true;
3162
- log(`
3163
- >>> [Tool Start: ${event.toolName}] >>>`);
3164
- log("Args:", JSON.stringify(event.args, null, 2));
3165
- onEvent({
3166
- type: "tool_start",
3167
- toolName: event.toolName,
3168
- toolInput: event.args
3169
- });
3170
- break;
3171
- case "tool_execution_end":
3172
- turnHadVisibleOutput = true;
3173
- log(`<<< [Tool End: ${event.toolName}] <<<`);
3174
- log(`Error: ${event.isError ? "Yes" : "No"}`);
3175
- onEvent({
3176
- type: "tool_end",
3177
- toolName: event.toolName,
3178
- isError: event.isError,
3179
- result: event.result
3180
- });
3181
- break;
3182
- case "agent_end":
3183
- log("\n=== [AGENT SESSION END] ===\n");
3184
- onEvent({ type: "agent_end" });
3185
- break;
3186
- }
3187
- });
3188
- try {
3857
+ break;
3858
+ case "agent_end":
3859
+ log("\n=== [AGENT SESSION END] ===\n");
3860
+ onEvent({ type: "agent_end" });
3861
+ break;
3862
+ }
3863
+ });
3189
3864
  let promptText = text;
3190
3865
  const promptOptions = {};
3191
3866
  if (attachments && attachments.length > 0) {
@@ -3213,15 +3888,17 @@ ${text}`;
3213
3888
  };
3214
3889
  }
3215
3890
  if (diagnostics && !diagnostics.hasText && diagnostics.toolCalls === 0 && !turnHadVisibleOutput) {
3891
+ const errorMessage = "Assistant returned no visible output. Check the server logs for details.";
3216
3892
  return {
3217
3893
  stopReason: diagnostics.stopReason,
3218
- errorMessage: "Assistant returned no visible output. Check the server logs for details."
3894
+ errorMessage
3219
3895
  };
3220
3896
  }
3221
3897
  return { stopReason: diagnostics?.stopReason ?? "unknown" };
3222
3898
  } finally {
3223
3899
  cs.running = false;
3224
- this.fileOutputCallbackRef.current = null;
3900
+ cs.fileOutputCallbackRef.current = null;
3901
+ cs.finalArtifactsSaveCallbackRef.current = null;
3225
3902
  unsubscribe();
3226
3903
  }
3227
3904
  };
@@ -3241,9 +3918,9 @@ ${text}`;
3241
3918
  this.channels.delete(channelId);
3242
3919
  }
3243
3920
  const { rootDir } = this.options;
3244
- const sessionDir = path9.resolve(rootDir, "data", "sessions", channelId);
3245
- if (fs9.existsSync(sessionDir)) {
3246
- fs9.rmSync(sessionDir, { recursive: true, force: true });
3921
+ const sessionDir = path13.resolve(rootDir, "data", "sessions", channelId);
3922
+ if (fs13.existsSync(sessionDir)) {
3923
+ fs13.rmSync(sessionDir, { recursive: true, force: true });
3247
3924
  log(`[PackAgent] Cleared session dir: ${sessionDir}`);
3248
3925
  }
3249
3926
  return {
@@ -3265,42 +3942,437 @@ ${text}`;
3265
3942
  return { success: false, message: `Unknown command: ${command}` };
3266
3943
  }
3267
3944
  }
3268
- abort(channelId) {
3269
- const cs = this.channels.get(channelId);
3270
- if (cs?.running) {
3271
- cs.session.abort?.();
3945
+ abort(channelId) {
3946
+ const cs = this.channels.get(channelId);
3947
+ if (cs?.running) {
3948
+ cs.session.abort?.();
3949
+ }
3950
+ }
3951
+ isRunning(channelId) {
3952
+ return this.channels.get(channelId)?.running ?? false;
3953
+ }
3954
+ dispose(channelId) {
3955
+ const cs = this.channels.get(channelId);
3956
+ if (cs) {
3957
+ cs.session.dispose();
3958
+ this.channels.delete(channelId);
3959
+ }
3960
+ }
3961
+ /** Reserved: list all sessions */
3962
+ listSessions() {
3963
+ return [];
3964
+ }
3965
+ /** Reserved: restore a historical session */
3966
+ async restoreSession(_sessionId) {
3967
+ }
3968
+ getActiveChannelIds() {
3969
+ return Array.from(this.channels.keys());
3970
+ }
3971
+ };
3972
+
3973
+ // src/runtime/adapters/web.ts
3974
+ import fs15 from "fs";
3975
+ import path15 from "path";
3976
+ import { WebSocketServer } from "ws";
3977
+ init_commands();
3978
+
3979
+ // src/runtime/services/conversation.ts
3980
+ import fs14 from "fs";
3981
+ import path14 from "path";
3982
+ import {
3983
+ parseSessionEntries
3984
+ } from "@mariozechner/pi-coding-agent";
3985
+ var DEFAULT_WEB_CHANNEL_ID = "web";
3986
+ var ConversationService = class {
3987
+ constructor(rootDir) {
3988
+ this.rootDir = rootDir;
3989
+ }
3990
+ /**
3991
+ * Scan data/sessions and return conversation summaries sorted by recency.
3992
+ */
3993
+ listConversations(activeChannels, options = {}) {
3994
+ const {
3995
+ includeDefaultWeb = false,
3996
+ includeLegacyWeb = true,
3997
+ allowedPlatforms
3998
+ } = options;
3999
+ const sessionsDir = path14.resolve(this.rootDir, "data", "sessions");
4000
+ const channelIds = new Set(activeChannels);
4001
+ const allowedPlatformSet = allowedPlatforms ? new Set(allowedPlatforms) : null;
4002
+ if (includeDefaultWeb) {
4003
+ channelIds.add(DEFAULT_WEB_CHANNEL_ID);
4004
+ }
4005
+ if (fs14.existsSync(sessionsDir)) {
4006
+ for (const entry of fs14.readdirSync(sessionsDir)) {
4007
+ const channelDir = path14.join(sessionsDir, entry);
4008
+ try {
4009
+ if (!fs14.statSync(channelDir).isDirectory()) {
4010
+ continue;
4011
+ }
4012
+ const platform = this.detectPlatform(entry);
4013
+ if (allowedPlatformSet && !allowedPlatformSet.has(platform)) {
4014
+ continue;
4015
+ }
4016
+ if (!includeLegacyWeb && this.isLegacyWebConversation(entry)) {
4017
+ continue;
4018
+ }
4019
+ channelIds.add(entry);
4020
+ } catch {
4021
+ }
4022
+ }
4023
+ }
4024
+ const results = [];
4025
+ for (const channelId of channelIds) {
4026
+ const platform = this.detectPlatform(channelId);
4027
+ if (allowedPlatformSet && !allowedPlatformSet.has(platform)) {
4028
+ continue;
4029
+ }
4030
+ if (!includeLegacyWeb && this.isLegacyWebConversation(channelId)) {
4031
+ continue;
4032
+ }
4033
+ const channelDir = path14.join(sessionsDir, channelId);
4034
+ const sessionFile = this.findLatestSessionFile(channelDir);
4035
+ let messageCount = 0;
4036
+ let lastMessageAt = "";
4037
+ let lastMessagePreview = "";
4038
+ if (sessionFile) {
4039
+ const entries = this.loadEntries(sessionFile);
4040
+ const messages = entries.filter(
4041
+ (entry) => entry.type === "message"
4042
+ );
4043
+ messageCount = messages.length;
4044
+ const lastMessage = messages[messages.length - 1];
4045
+ if (lastMessage) {
4046
+ lastMessageAt = lastMessage.timestamp;
4047
+ lastMessagePreview = this.extractTextPreview(lastMessage, 100);
4048
+ }
4049
+ }
4050
+ results.push({
4051
+ channelId,
4052
+ platform,
4053
+ sessionFile,
4054
+ messageCount,
4055
+ lastMessageAt,
4056
+ lastMessagePreview
4057
+ });
4058
+ }
4059
+ return results.sort((a, b) => {
4060
+ if (a.channelId === DEFAULT_WEB_CHANNEL_ID && b.channelId !== DEFAULT_WEB_CHANNEL_ID) {
4061
+ return -1;
4062
+ }
4063
+ if (b.channelId === DEFAULT_WEB_CHANNEL_ID && a.channelId !== DEFAULT_WEB_CHANNEL_ID) {
4064
+ return 1;
4065
+ }
4066
+ const recency = (b.lastMessageAt || "").localeCompare(
4067
+ a.lastMessageAt || ""
4068
+ );
4069
+ if (recency !== 0) return recency;
4070
+ return a.channelId.localeCompare(b.channelId);
4071
+ });
4072
+ }
4073
+ /**
4074
+ * Load latest messages for a channel in a simplified format.
4075
+ */
4076
+ getMessages(channelId, limit = 100) {
4077
+ const channelDir = path14.resolve(
4078
+ this.rootDir,
4079
+ "data",
4080
+ "sessions",
4081
+ channelId
4082
+ );
4083
+ const sessionFile = this.findLatestSessionFile(channelDir);
4084
+ if (!sessionFile) return [];
4085
+ const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 100;
4086
+ if (safeLimit === 0) return [];
4087
+ const entries = this.loadEntries(sessionFile);
4088
+ const toolResultsById = this.collectToolResultStates(entries);
4089
+ const messages = [];
4090
+ let pendingBlocks = [];
4091
+ let pendingToolCalls = [];
4092
+ let pendingMessageId = "";
4093
+ let pendingTimestamp = "";
4094
+ const flushPendingAssistant = () => {
4095
+ if (pendingBlocks.length === 0 && pendingToolCalls.length === 0) {
4096
+ return;
4097
+ }
4098
+ messages.push({
4099
+ id: pendingMessageId || `assistant-${messages.length + 1}`,
4100
+ role: "assistant",
4101
+ text: "",
4102
+ timestamp: pendingTimestamp || (/* @__PURE__ */ new Date(0)).toISOString(),
4103
+ toolCalls: pendingToolCalls.length > 0 ? pendingToolCalls : void 0,
4104
+ blocks: pendingBlocks.length > 0 ? pendingBlocks : void 0
4105
+ });
4106
+ pendingBlocks = [];
4107
+ pendingToolCalls = [];
4108
+ pendingMessageId = "";
4109
+ pendingTimestamp = "";
4110
+ };
4111
+ for (const entry of entries) {
4112
+ if (entry.type !== "message") continue;
4113
+ const role = entry.message?.role;
4114
+ if (role === "user") {
4115
+ flushPendingAssistant();
4116
+ const text2 = this.extractText(entry.message);
4117
+ if (!text2) continue;
4118
+ messages.push({
4119
+ id: entry.id,
4120
+ role,
4121
+ text: text2,
4122
+ timestamp: entry.timestamp
4123
+ });
4124
+ continue;
4125
+ }
4126
+ if (role !== "assistant") continue;
4127
+ const text = this.extractText(entry.message);
4128
+ const toolCalls = this.extractToolCalls(entry.message, toolResultsById);
4129
+ const blocks = this.extractBlocks(
4130
+ entry.id,
4131
+ entry.message,
4132
+ toolResultsById
4133
+ );
4134
+ if (!text) {
4135
+ if (blocks.length > 0) {
4136
+ pendingBlocks = [...pendingBlocks, ...blocks];
4137
+ }
4138
+ if (toolCalls?.length) {
4139
+ pendingToolCalls = this.mergeToolCalls(pendingToolCalls, toolCalls);
4140
+ }
4141
+ if (!pendingMessageId) {
4142
+ pendingMessageId = entry.id;
4143
+ pendingTimestamp = entry.timestamp;
4144
+ }
4145
+ continue;
4146
+ }
4147
+ const mergedBlocks = pendingBlocks.length > 0 ? [...pendingBlocks, ...blocks] : blocks;
4148
+ const mergedToolCalls = this.mergeToolCalls(pendingToolCalls, toolCalls);
4149
+ messages.push({
4150
+ id: entry.id,
4151
+ role,
4152
+ text,
4153
+ timestamp: entry.timestamp,
4154
+ toolCalls: mergedToolCalls.length > 0 ? mergedToolCalls : void 0,
4155
+ blocks: mergedBlocks.length > 0 ? mergedBlocks : void 0
4156
+ });
4157
+ pendingBlocks = [];
4158
+ pendingToolCalls = [];
4159
+ pendingMessageId = "";
4160
+ pendingTimestamp = "";
4161
+ }
4162
+ flushPendingAssistant();
4163
+ return messages.slice(-safeLimit);
4164
+ }
4165
+ findLatestSessionFile(channelDir) {
4166
+ if (!fs14.existsSync(channelDir)) return null;
4167
+ let stats;
4168
+ try {
4169
+ stats = fs14.statSync(channelDir);
4170
+ } catch {
4171
+ return null;
4172
+ }
4173
+ if (!stats.isDirectory()) return null;
4174
+ const files = fs14.readdirSync(channelDir).filter((file) => file.endsWith(".jsonl")).sort((a, b) => b.localeCompare(a));
4175
+ return files[0] ? path14.join(channelDir, files[0]) : null;
4176
+ }
4177
+ loadEntries(filePath) {
4178
+ try {
4179
+ const content = fs14.readFileSync(filePath, "utf-8");
4180
+ const fileEntries = parseSessionEntries(content);
4181
+ return fileEntries.filter(
4182
+ (entry) => entry.type !== "session"
4183
+ );
4184
+ } catch (err) {
4185
+ console.warn(`[ConversationService] Failed to load ${filePath}:`, err);
4186
+ return [];
4187
+ }
4188
+ }
4189
+ extractText(message) {
4190
+ if (!message?.content) return "";
4191
+ if (typeof message.content === "string") return message.content.trim();
4192
+ if (!Array.isArray(message.content)) return "";
4193
+ return message.content.filter((item) => item?.type === "text").map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
4194
+ }
4195
+ extractTextPreview(entry, maxLen) {
4196
+ const text = this.extractText(entry.message);
4197
+ return text.length > maxLen ? `${text.slice(0, maxLen)}\u2026` : text;
4198
+ }
4199
+ collectToolResultStates(entries) {
4200
+ const toolResultsById = /* @__PURE__ */ new Map();
4201
+ for (const entry of entries) {
4202
+ if (entry.type !== "message") continue;
4203
+ if (entry.message?.role !== "toolResult") continue;
4204
+ if (typeof entry.message?.toolCallId !== "string" || !entry.message.toolCallId) {
4205
+ continue;
4206
+ }
4207
+ toolResultsById.set(entry.message.toolCallId, {
4208
+ isError: entry.message?.isError === true,
4209
+ result: this.extractToolResultValue(entry.message)
4210
+ });
4211
+ }
4212
+ return toolResultsById;
4213
+ }
4214
+ extractToolCalls(message, toolResultsById) {
4215
+ if (!Array.isArray(message?.content)) return void 0;
4216
+ const toolCalls = message.content.filter((item) => item?.type === "toolCall").map((item) => {
4217
+ const id = typeof item?.id === "string" && item.id ? item.id : "unknown";
4218
+ const name = typeof item?.name === "string" && item.name ? item.name : "unknown";
4219
+ const toolCall = {
4220
+ id,
4221
+ name,
4222
+ isError: toolResultsById.get(id)?.isError === true
4223
+ };
4224
+ if (name === "send_file") {
4225
+ const args = this.extractSendFileArguments(item?.arguments);
4226
+ if (args) {
4227
+ toolCall.arguments = args;
4228
+ }
4229
+ }
4230
+ return toolCall;
4231
+ });
4232
+ return toolCalls.length > 0 ? toolCalls : void 0;
4233
+ }
4234
+ extractBlocks(messageId, message, toolResultsById) {
4235
+ if (!Array.isArray(message?.content)) {
4236
+ return [];
4237
+ }
4238
+ const blocks = [];
4239
+ message.content.forEach((item, index) => {
4240
+ if (item?.type === "thinking") {
4241
+ const thinkingText = this.extractThinkingText(item);
4242
+ if (thinkingText) {
4243
+ blocks.push({
4244
+ id: `${messageId}-thinking-${index}`,
4245
+ type: "thinking",
4246
+ text: thinkingText
4247
+ });
4248
+ }
4249
+ return;
4250
+ }
4251
+ if (item?.type !== "toolCall") {
4252
+ return;
4253
+ }
4254
+ const toolCallId = typeof item?.id === "string" && item.id ? item.id : `${messageId}-tool-${index}`;
4255
+ const toolName = typeof item?.name === "string" && item.name ? item.name : "unknown";
4256
+ const toolResult = toolResultsById.get(toolCallId);
4257
+ const sendFileArgs = toolName === "send_file" ? this.extractSendFileArguments(item?.arguments) : void 0;
4258
+ if (toolName === "send_file" && !toolResult?.isError && sendFileArgs?.filePath) {
4259
+ blocks.push({
4260
+ id: toolCallId,
4261
+ type: "file",
4262
+ filename: this.getFileBaseName(sendFileArgs.filePath),
4263
+ filePath: sendFileArgs.filePath,
4264
+ caption: sendFileArgs.caption
4265
+ });
4266
+ return;
4267
+ }
4268
+ blocks.push({
4269
+ id: toolCallId,
4270
+ type: "tool",
4271
+ toolCallId,
4272
+ toolName,
4273
+ toolInput: item?.arguments,
4274
+ result: toolResult?.result,
4275
+ isError: toolResult?.isError === true,
4276
+ status: toolResult ? "done" : "running"
4277
+ });
4278
+ });
4279
+ return blocks;
4280
+ }
4281
+ extractThinkingText(item) {
4282
+ if (typeof item?.thinking === "string" && item.thinking.trim()) {
4283
+ return item.thinking.trim();
4284
+ }
4285
+ if (typeof item?.thinkingSignature !== "string" || !item.thinkingSignature) {
4286
+ return "";
4287
+ }
4288
+ try {
4289
+ const parsed = JSON.parse(item.thinkingSignature);
4290
+ if (!Array.isArray(parsed?.summary)) {
4291
+ return "";
4292
+ }
4293
+ return parsed.summary.map(
4294
+ (summaryItem) => typeof summaryItem?.text === "string" ? summaryItem.text.trim() : ""
4295
+ ).filter(Boolean).join("\n\n").trim();
4296
+ } catch {
4297
+ return "";
4298
+ }
4299
+ }
4300
+ extractToolResultValue(message) {
4301
+ if (!message?.content) {
4302
+ return void 0;
4303
+ }
4304
+ if (typeof message.content === "string") {
4305
+ return this.parsePossibleJson(message.content);
4306
+ }
4307
+ if (!Array.isArray(message.content)) {
4308
+ return message.content;
4309
+ }
4310
+ const textContent = message.content.filter((item) => item?.type === "text").map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
4311
+ if (textContent) {
4312
+ return this.parsePossibleJson(textContent);
4313
+ }
4314
+ return message.content;
4315
+ }
4316
+ parsePossibleJson(value) {
4317
+ const trimmed = value.trim();
4318
+ if (!trimmed) {
4319
+ return "";
4320
+ }
4321
+ try {
4322
+ return JSON.parse(trimmed);
4323
+ } catch {
4324
+ return trimmed;
4325
+ }
4326
+ }
4327
+ extractSendFileArguments(rawArguments) {
4328
+ if (!rawArguments || typeof rawArguments !== "object") {
4329
+ return void 0;
3272
4330
  }
4331
+ const maybeArgs = rawArguments;
4332
+ const filePath = typeof maybeArgs.filePath === "string" && maybeArgs.filePath ? maybeArgs.filePath : void 0;
4333
+ const caption = typeof maybeArgs.caption === "string" && maybeArgs.caption ? maybeArgs.caption : void 0;
4334
+ if (!filePath && !caption) {
4335
+ return void 0;
4336
+ }
4337
+ return {
4338
+ filePath,
4339
+ caption
4340
+ };
3273
4341
  }
3274
- isRunning(channelId) {
3275
- return this.channels.get(channelId)?.running ?? false;
4342
+ hasVisibleSendFileToolCall(toolCalls) {
4343
+ return Boolean(
4344
+ toolCalls?.some(
4345
+ (toolCall) => toolCall.name === "send_file" && !toolCall.isError && typeof toolCall.arguments?.filePath === "string" && toolCall.arguments.filePath.length > 0
4346
+ )
4347
+ );
3276
4348
  }
3277
- dispose(channelId) {
3278
- const cs = this.channels.get(channelId);
3279
- if (cs) {
3280
- cs.session.dispose();
3281
- this.channels.delete(channelId);
4349
+ mergeToolCalls(left, right) {
4350
+ const merged = [...left || [], ...right || []];
4351
+ const byId = /* @__PURE__ */ new Map();
4352
+ for (const toolCall of merged) {
4353
+ byId.set(toolCall.id, toolCall);
3282
4354
  }
4355
+ return [...byId.values()];
3283
4356
  }
3284
- /** Reserved: list all sessions */
3285
- listSessions() {
3286
- return [];
4357
+ getFileBaseName(filePath) {
4358
+ const normalized = filePath.replace(/\\/g, "/");
4359
+ const parts = normalized.split("/").filter(Boolean);
4360
+ return parts[parts.length - 1] || filePath;
3287
4361
  }
3288
- /** Reserved: restore a historical session */
3289
- async restoreSession(_sessionId) {
4362
+ detectPlatform(channelId) {
4363
+ if (channelId.startsWith("telegram-")) return "telegram";
4364
+ if (channelId.startsWith("slack-")) return "slack";
4365
+ if (channelId.startsWith("scheduler-")) return "scheduler";
4366
+ return "web";
3290
4367
  }
3291
- getActiveChannelIds() {
3292
- return Array.from(this.channels.keys());
4368
+ isLegacyWebConversation(channelId) {
4369
+ return channelId.startsWith("web-");
3293
4370
  }
3294
4371
  };
3295
4372
 
3296
4373
  // src/runtime/adapters/web.ts
3297
- init_config();
3298
- init_commands();
3299
- import fs10 from "fs";
3300
- import path10 from "path";
3301
- import { WebSocketServer } from "ws";
3302
4374
  function getPackConfig(rootDir) {
3303
- const raw = fs10.readFileSync(path10.join(rootDir, "skillpack.json"), "utf-8");
4375
+ const raw = fs15.readFileSync(path15.join(rootDir, "skillpack.json"), "utf-8");
3304
4376
  return JSON.parse(raw);
3305
4377
  }
3306
4378
  function parseCommand(text) {
@@ -3322,15 +4394,30 @@ function getRuntimeConfigSignature(config) {
3322
4394
  slackAppToken: config.adapters?.slack?.appToken || ""
3323
4395
  });
3324
4396
  }
4397
+ function parsePositiveInt(value, fallback) {
4398
+ const parsed = Number(value);
4399
+ if (!Number.isFinite(parsed) || parsed <= 0) {
4400
+ return fallback;
4401
+ }
4402
+ return Math.floor(parsed);
4403
+ }
4404
+ function resolveDownloadFilePath(rootDir, filePath) {
4405
+ const resolvedPath = path15.isAbsolute(filePath) ? path15.resolve(filePath) : path15.resolve(rootDir, filePath);
4406
+ return isWithinDirectory(rootDir, resolvedPath) ? resolvedPath : null;
4407
+ }
3325
4408
  var WebAdapter = class {
3326
4409
  name = "web";
3327
4410
  wss = null;
3328
4411
  agent = null;
3329
4412
  ipcBroadcaster = null;
4413
+ conversationService = null;
4414
+ socketsByChannel = /* @__PURE__ */ new Map();
3330
4415
  async start(ctx) {
3331
4416
  const { agent, server, app, rootDir, lifecycle } = ctx;
3332
4417
  this.agent = agent;
3333
4418
  this.ipcBroadcaster = ctx.ipcBroadcaster ?? null;
4419
+ this.conversationService = new ConversationService(rootDir);
4420
+ const resultsQueryService = ctx.resultsQueryService ?? null;
3334
4421
  const currentConf = configManager.getConfig();
3335
4422
  let apiKey = currentConf.apiKey || "";
3336
4423
  let currentProvider = currentConf.provider || "openai";
@@ -3448,12 +4535,42 @@ var WebAdapter = class {
3448
4535
  app.delete("/api/chat", (_req, res) => {
3449
4536
  res.json({ success: true });
3450
4537
  });
3451
- app.get("/api/sessions", (_req, res) => {
3452
- const sessions = agent.listSessions();
3453
- res.json(sessions);
4538
+ const getWebConversations = () => {
4539
+ const activeChannels = new Set(agent.getActiveChannelIds());
4540
+ return this.conversationService.listConversations(activeChannels, {
4541
+ includeDefaultWeb: true,
4542
+ includeLegacyWeb: false,
4543
+ allowedPlatforms: ["web"]
4544
+ });
4545
+ };
4546
+ app.get("/api/conversations", (_req, res) => {
4547
+ res.json(getWebConversations());
4548
+ });
4549
+ app.post("/api/conversations", (_req, res) => {
4550
+ res.json({ channelId: DEFAULT_WEB_CHANNEL_ID });
4551
+ });
4552
+ app.get("/api/conversations/:channelId/messages", (req, res) => {
4553
+ if (req.params.channelId !== DEFAULT_WEB_CHANNEL_ID) {
4554
+ res.status(404).json({ error: "Conversation not found" });
4555
+ return;
4556
+ }
4557
+ res.json(
4558
+ this.conversationService.getMessages(
4559
+ req.params.channelId,
4560
+ parsePositiveInt(req.query.limit, 100)
4561
+ )
4562
+ );
3454
4563
  });
3455
- app.get("/api/sessions/:id", (_req, res) => {
3456
- res.status(501).json({ error: "Not implemented yet" });
4564
+ app.get("/api/results/artifacts", async (req, res) => {
4565
+ if (!resultsQueryService) {
4566
+ res.status(503).json({ error: "Results query service is not available" });
4567
+ return;
4568
+ }
4569
+ res.json(await resultsQueryService.listRecentArtifacts({
4570
+ channelId: typeof req.query.channelId === "string" ? req.query.channelId : void 0,
4571
+ limit: parsePositiveInt(req.query.limit, 100),
4572
+ offset: parsePositiveInt(req.query.offset, 0)
4573
+ }));
3457
4574
  });
3458
4575
  app.get("/api/files", (req, res) => {
3459
4576
  const filePath = req.query.path;
@@ -3461,23 +4578,22 @@ var WebAdapter = class {
3461
4578
  res.status(400).json({ error: "Missing 'path' query parameter" });
3462
4579
  return;
3463
4580
  }
3464
- const resolvedPath = path10.resolve(filePath);
3465
- const dataDir = path10.resolve(rootDir, "data");
3466
- if (!resolvedPath.startsWith(dataDir)) {
4581
+ const resolvedPath = resolveDownloadFilePath(rootDir, filePath);
4582
+ if (!resolvedPath) {
3467
4583
  res.status(403).json({ error: "Access denied" });
3468
4584
  return;
3469
4585
  }
3470
- if (!fs10.existsSync(resolvedPath)) {
4586
+ if (!fs15.existsSync(resolvedPath)) {
3471
4587
  res.status(404).json({ error: "File not found" });
3472
4588
  return;
3473
4589
  }
3474
- const filename = path10.basename(resolvedPath);
4590
+ const filename = path15.basename(resolvedPath);
3475
4591
  res.setHeader("Content-Type", "application/octet-stream");
3476
4592
  res.setHeader(
3477
4593
  "Content-Disposition",
3478
4594
  `attachment; filename="${filename}"`
3479
4595
  );
3480
- fs10.createReadStream(resolvedPath).pipe(res);
4596
+ fs15.createReadStream(resolvedPath).pipe(res);
3481
4597
  });
3482
4598
  const getScheduler = () => {
3483
4599
  const schedulerAdapter = ctx.adapterMap?.get("scheduler");
@@ -3571,7 +4687,8 @@ var WebAdapter = class {
3571
4687
  ws.close();
3572
4688
  return;
3573
4689
  }
3574
- const channelId = `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4690
+ const requestedChannelId = url.searchParams.get("channelId");
4691
+ const channelId = requestedChannelId && requestedChannelId === DEFAULT_WEB_CHANNEL_ID ? requestedChannelId : DEFAULT_WEB_CHANNEL_ID;
3575
4692
  this.handleWsConnection(ws, channelId, agent);
3576
4693
  });
3577
4694
  console.log("[WebAdapter] Started");
@@ -3584,12 +4701,29 @@ var WebAdapter = class {
3584
4701
  this.wss.close();
3585
4702
  this.wss = null;
3586
4703
  }
4704
+ this.socketsByChannel.clear();
3587
4705
  console.log("[WebAdapter] Stopped");
3588
4706
  }
4707
+ async sendMessage(channelId, text) {
4708
+ const sockets = this.socketsByChannel.get(channelId);
4709
+ const activeSockets = [...sockets || []].filter(
4710
+ (socket) => socket.readyState === socket.OPEN
4711
+ );
4712
+ if (activeSockets.length === 0) {
4713
+ throw new Error(`[Web] No active WebSocket clients for channelId: ${channelId}`);
4714
+ }
4715
+ for (const socket of activeSockets) {
4716
+ sendWsEvent(socket, { type: "message_start", role: "assistant" });
4717
+ sendWsEvent(socket, { type: "text_delta", delta: text });
4718
+ sendWsEvent(socket, { type: "message_end", role: "assistant" });
4719
+ socket.send(JSON.stringify({ done: true }));
4720
+ }
4721
+ }
3589
4722
  // -------------------------------------------------------------------------
3590
4723
  // WebSocket message handler
3591
4724
  // -------------------------------------------------------------------------
3592
4725
  handleWsConnection(ws, channelId, agent) {
4726
+ this.addSocket(channelId, ws);
3593
4727
  ws.on("message", async (data) => {
3594
4728
  try {
3595
4729
  const payload = JSON.parse(data.toString());
@@ -3619,154 +4753,27 @@ var WebAdapter = class {
3619
4753
  }
3620
4754
  });
3621
4755
  ws.on("close", () => {
3622
- agent.dispose(channelId);
3623
- });
3624
- }
3625
- };
3626
-
3627
- // src/runtime/adapters/ipc.ts
3628
- init_config();
3629
-
3630
- // src/runtime/services/conversation.ts
3631
- import fs11 from "fs";
3632
- import path11 from "path";
3633
- import {
3634
- parseSessionEntries
3635
- } from "@mariozechner/pi-coding-agent";
3636
- var ConversationService = class {
3637
- constructor(rootDir) {
3638
- this.rootDir = rootDir;
3639
- }
3640
- /**
3641
- * Scan data/sessions and return conversation summaries sorted by recency.
3642
- */
3643
- listConversations(activeChannels) {
3644
- const sessionsDir = path11.resolve(this.rootDir, "data", "sessions");
3645
- const channelIds = new Set(activeChannels);
3646
- if (fs11.existsSync(sessionsDir)) {
3647
- for (const entry of fs11.readdirSync(sessionsDir)) {
3648
- const channelDir = path11.join(sessionsDir, entry);
3649
- try {
3650
- if (fs11.statSync(channelDir).isDirectory()) {
3651
- channelIds.add(entry);
3652
- }
3653
- } catch {
3654
- }
3655
- }
3656
- }
3657
- const results = [];
3658
- for (const channelId of channelIds) {
3659
- const channelDir = path11.join(sessionsDir, channelId);
3660
- const sessionFile = this.findLatestSessionFile(channelDir);
3661
- let messageCount = 0;
3662
- let lastMessageAt = "";
3663
- let lastMessagePreview = "";
3664
- if (sessionFile) {
3665
- const entries = this.loadEntries(sessionFile);
3666
- const messages = entries.filter(
3667
- (entry) => entry.type === "message"
3668
- );
3669
- messageCount = messages.length;
3670
- const lastMessage = messages[messages.length - 1];
3671
- if (lastMessage) {
3672
- lastMessageAt = lastMessage.timestamp;
3673
- lastMessagePreview = this.extractTextPreview(lastMessage, 100);
3674
- }
4756
+ this.removeSocket(channelId, ws);
4757
+ if (channelId !== DEFAULT_WEB_CHANNEL_ID) {
4758
+ agent.dispose(channelId);
3675
4759
  }
3676
- results.push({
3677
- channelId,
3678
- platform: this.detectPlatform(channelId),
3679
- sessionFile,
3680
- messageCount,
3681
- lastMessageAt,
3682
- lastMessagePreview
3683
- });
3684
- }
3685
- return results.sort((a, b) => {
3686
- const recency = (b.lastMessageAt || "").localeCompare(a.lastMessageAt || "");
3687
- if (recency !== 0) return recency;
3688
- return a.channelId.localeCompare(b.channelId);
3689
4760
  });
3690
4761
  }
3691
- /**
3692
- * Load latest messages for a channel in a simplified format.
3693
- */
3694
- getMessages(channelId, limit = 100) {
3695
- const channelDir = path11.resolve(
3696
- this.rootDir,
3697
- "data",
3698
- "sessions",
3699
- channelId
3700
- );
3701
- const sessionFile = this.findLatestSessionFile(channelDir);
3702
- if (!sessionFile) return [];
3703
- const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 100;
3704
- if (safeLimit === 0) return [];
3705
- const entries = this.loadEntries(sessionFile);
3706
- const messages = [];
3707
- for (const entry of entries) {
3708
- if (entry.type !== "message") continue;
3709
- const role = entry.message?.role;
3710
- if (role !== "user" && role !== "assistant") continue;
3711
- const text = this.extractText(entry.message);
3712
- if (!text) continue;
3713
- const toolCalls = role === "assistant" ? this.extractToolCallSummaries(entry.message) : void 0;
3714
- messages.push({
3715
- id: entry.id,
3716
- role,
3717
- text,
3718
- timestamp: entry.timestamp,
3719
- toolCalls
3720
- });
3721
- }
3722
- return messages.slice(-safeLimit);
4762
+ addSocket(channelId, ws) {
4763
+ const sockets = this.socketsByChannel.get(channelId) ?? /* @__PURE__ */ new Set();
4764
+ sockets.add(ws);
4765
+ this.socketsByChannel.set(channelId, sockets);
3723
4766
  }
3724
- findLatestSessionFile(channelDir) {
3725
- if (!fs11.existsSync(channelDir)) return null;
3726
- let stats;
3727
- try {
3728
- stats = fs11.statSync(channelDir);
3729
- } catch {
3730
- return null;
4767
+ removeSocket(channelId, ws) {
4768
+ const sockets = this.socketsByChannel.get(channelId);
4769
+ if (!sockets) {
4770
+ return;
3731
4771
  }
3732
- if (!stats.isDirectory()) return null;
3733
- const files = fs11.readdirSync(channelDir).filter((file) => file.endsWith(".jsonl")).sort((a, b) => b.localeCompare(a));
3734
- return files[0] ? path11.join(channelDir, files[0]) : null;
3735
- }
3736
- loadEntries(filePath) {
3737
- try {
3738
- const content = fs11.readFileSync(filePath, "utf-8");
3739
- const fileEntries = parseSessionEntries(content);
3740
- return fileEntries.filter((entry) => entry.type !== "session");
3741
- } catch (err) {
3742
- console.warn(`[ConversationService] Failed to load ${filePath}:`, err);
3743
- return [];
4772
+ sockets.delete(ws);
4773
+ if (sockets.size === 0) {
4774
+ this.socketsByChannel.delete(channelId);
3744
4775
  }
3745
4776
  }
3746
- extractText(message) {
3747
- if (!message?.content) return "";
3748
- if (typeof message.content === "string") return message.content.trim();
3749
- if (!Array.isArray(message.content)) return "";
3750
- return message.content.filter((item) => item?.type === "text").map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
3751
- }
3752
- extractTextPreview(entry, maxLen) {
3753
- const text = this.extractText(entry.message);
3754
- return text.length > maxLen ? `${text.slice(0, maxLen)}\u2026` : text;
3755
- }
3756
- extractToolCallSummaries(message) {
3757
- if (!Array.isArray(message?.content)) return void 0;
3758
- const toolCalls = message.content.filter((item) => item?.type === "toolCall").map((item) => ({
3759
- name: typeof item?.name === "string" && item.name ? item.name : "unknown",
3760
- isError: false
3761
- }));
3762
- return toolCalls.length > 0 ? toolCalls : void 0;
3763
- }
3764
- detectPlatform(channelId) {
3765
- if (channelId.startsWith("telegram-")) return "telegram";
3766
- if (channelId.startsWith("slack-")) return "slack";
3767
- if (channelId.startsWith("scheduler-")) return "scheduler";
3768
- return "web";
3769
- }
3770
4777
  };
3771
4778
 
3772
4779
  // src/runtime/adapters/ipc.ts
@@ -3777,6 +4784,7 @@ var IpcAdapter = class {
3777
4784
  rootDir = "";
3778
4785
  adapterMap = null;
3779
4786
  conversationService = null;
4787
+ resultsQueryService = null;
3780
4788
  createdChannels = /* @__PURE__ */ new Set();
3781
4789
  messageListener;
3782
4790
  started = false;
@@ -3788,6 +4796,7 @@ var IpcAdapter = class {
3788
4796
  this.rootDir = ctx.rootDir;
3789
4797
  this.adapterMap = ctx.adapterMap ?? null;
3790
4798
  this.conversationService = new ConversationService(ctx.rootDir);
4799
+ this.resultsQueryService = ctx.resultsQueryService ?? null;
3791
4800
  this.messageListener = (message) => {
3792
4801
  if (!this.isIpcRequest(message)) return;
3793
4802
  void this.handleRequest(message);
@@ -3846,12 +4855,15 @@ var IpcAdapter = class {
3846
4855
  for (const channelId of this.createdChannels) {
3847
4856
  activeChannels.add(channelId);
3848
4857
  }
3849
- const conversations = this.conversationService.listConversations(activeChannels);
4858
+ const conversations = this.conversationService.listConversations(activeChannels, {
4859
+ includeDefaultWeb: true,
4860
+ includeLegacyWeb: false
4861
+ });
3850
4862
  this.reply(request.id, conversations);
3851
4863
  return;
3852
4864
  }
3853
4865
  case "create_conversation": {
3854
- const channelId = `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4866
+ const channelId = DEFAULT_WEB_CHANNEL_ID;
3855
4867
  this.createdChannels.add(channelId);
3856
4868
  this.reply(request.id, { channelId });
3857
4869
  return;
@@ -3868,6 +4880,18 @@ var IpcAdapter = class {
3868
4880
  this.reply(request.id, messages);
3869
4881
  return;
3870
4882
  }
4883
+ case "get_recent_artifacts": {
4884
+ if (!this.resultsQueryService) {
4885
+ this.replyError(request.id, "Results query service is not available");
4886
+ return;
4887
+ }
4888
+ this.reply(request.id, await this.resultsQueryService.listRecentArtifacts({
4889
+ channelId: request.channelId,
4890
+ limit: request.limit,
4891
+ offset: request.offset
4892
+ }));
4893
+ return;
4894
+ }
3871
4895
  case "send_message": {
3872
4896
  if (!request.channelId || typeof request.channelId !== "string") {
3873
4897
  this.replyError(request.id, "channelId is required");
@@ -3950,6 +4974,48 @@ var IpcAdapter = class {
3950
4974
  this.reply(request.id, result);
3951
4975
  return;
3952
4976
  }
4977
+ case "update_scheduled_job": {
4978
+ const scheduler = this.getSchedulerAdapter();
4979
+ if (!scheduler) {
4980
+ this.replyError(request.id, "Scheduler adapter is not available");
4981
+ return;
4982
+ }
4983
+ const result = scheduler.updateJob(request.name, request.updates);
4984
+ if (!result.success) {
4985
+ this.replyError(request.id, result.message);
4986
+ return;
4987
+ }
4988
+ this.reply(request.id, result);
4989
+ return;
4990
+ }
4991
+ case "set_scheduled_job_enabled": {
4992
+ const scheduler = this.getSchedulerAdapter();
4993
+ if (!scheduler) {
4994
+ this.replyError(request.id, "Scheduler adapter is not available");
4995
+ return;
4996
+ }
4997
+ const result = scheduler.setEnabled(request.name, request.enabled);
4998
+ if (!result.success) {
4999
+ this.replyError(request.id, result.message);
5000
+ return;
5001
+ }
5002
+ this.reply(request.id, result);
5003
+ return;
5004
+ }
5005
+ case "trigger_scheduled_job": {
5006
+ const scheduler = this.getSchedulerAdapter();
5007
+ if (!scheduler) {
5008
+ this.replyError(request.id, "Scheduler adapter is not available");
5009
+ return;
5010
+ }
5011
+ const result = await scheduler.triggerJob(request.name);
5012
+ if (!result.success) {
5013
+ this.replyError(request.id, result.message);
5014
+ return;
5015
+ }
5016
+ this.reply(request.id, result);
5017
+ return;
5018
+ }
3953
5019
  case "remove_scheduled_job": {
3954
5020
  const scheduler = this.getSchedulerAdapter();
3955
5021
  if (!scheduler) {
@@ -3996,9 +5062,6 @@ var IpcAdapter = class {
3996
5062
  }
3997
5063
  };
3998
5064
 
3999
- // src/runtime/server.ts
4000
- init_config();
4001
-
4002
5065
  // src/runtime/lifecycle.ts
4003
5066
  var SHUTDOWN_EXIT_CODE = 64;
4004
5067
  var RESTART_EXIT_CODE = 75;
@@ -4072,28 +5135,28 @@ var Lifecycle = class {
4072
5135
 
4073
5136
  // src/runtime/registry.ts
4074
5137
  import crypto from "crypto";
4075
- import fs12 from "fs";
5138
+ import fs16 from "fs";
4076
5139
  import os from "os";
4077
- import path12 from "path";
4078
- var SKILLPACK_HOME = path12.join(os.homedir(), ".skillpack");
4079
- var LEGACY_REGISTRY_FILE = path12.join(SKILLPACK_HOME, "registry.json");
4080
- var REGISTRY_DIR = path12.join(SKILLPACK_HOME, "registry.d");
5140
+ import path16 from "path";
5141
+ var SKILLPACK_HOME = path16.join(os.homedir(), ".skillpack");
5142
+ var LEGACY_REGISTRY_FILE = path16.join(SKILLPACK_HOME, "registry.json");
5143
+ var REGISTRY_DIR = path16.join(SKILLPACK_HOME, "registry.d");
4081
5144
  var migrationChecked = false;
4082
5145
  function ensureHomeDir() {
4083
- if (!fs12.existsSync(SKILLPACK_HOME)) {
4084
- fs12.mkdirSync(SKILLPACK_HOME, { recursive: true });
5146
+ if (!fs16.existsSync(SKILLPACK_HOME)) {
5147
+ fs16.mkdirSync(SKILLPACK_HOME, { recursive: true });
4085
5148
  }
4086
5149
  }
4087
5150
  function ensureRegistryDir() {
4088
5151
  ensureHomeDir();
4089
- if (!fs12.existsSync(REGISTRY_DIR)) {
4090
- fs12.mkdirSync(REGISTRY_DIR, { recursive: true });
5152
+ if (!fs16.existsSync(REGISTRY_DIR)) {
5153
+ fs16.mkdirSync(REGISTRY_DIR, { recursive: true });
4091
5154
  }
4092
5155
  }
4093
5156
  function canonicalizeDir(dir) {
4094
- const resolved = path12.resolve(dir);
5157
+ const resolved = path16.resolve(dir);
4095
5158
  try {
4096
- return fs12.realpathSync(resolved);
5159
+ return fs16.realpathSync(resolved);
4097
5160
  } catch {
4098
5161
  return resolved;
4099
5162
  }
@@ -4102,7 +5165,7 @@ function hashDir(dir) {
4102
5165
  return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
4103
5166
  }
4104
5167
  function getEntryPathForCanonicalDir(dir) {
4105
- return path12.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
5168
+ return path16.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
4106
5169
  }
4107
5170
  function getEntryPath(dir) {
4108
5171
  ensureRegistryReady();
@@ -4110,11 +5173,11 @@ function getEntryPath(dir) {
4110
5173
  }
4111
5174
  function listEntryFiles() {
4112
5175
  ensureRegistryReady();
4113
- return fs12.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path12.join(REGISTRY_DIR, file));
5176
+ return fs16.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path16.join(REGISTRY_DIR, file));
4114
5177
  }
4115
5178
  function readEntryFile(filePath) {
4116
5179
  try {
4117
- const raw = fs12.readFileSync(filePath, "utf-8");
5180
+ const raw = fs16.readFileSync(filePath, "utf-8");
4118
5181
  const data = JSON.parse(raw);
4119
5182
  if (typeof data?.dir !== "string" || typeof data?.name !== "string" || typeof data?.version !== "string" || typeof data?.port !== "number" || typeof data?.pid !== "number" && data?.pid !== null || data?.status !== "running" && data?.status !== "stopped") {
4120
5183
  return null;
@@ -4147,8 +5210,8 @@ function writeEntryFile(entry) {
4147
5210
  };
4148
5211
  const entryPath = getEntryPathForCanonicalDir(normalized.dir);
4149
5212
  const tmpPath = createTmpPath(entryPath);
4150
- fs12.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
4151
- fs12.renameSync(tmpPath, entryPath);
5213
+ fs16.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
5214
+ fs16.renameSync(tmpPath, entryPath);
4152
5215
  }
4153
5216
  function migrateLegacyRegistryIfNeeded() {
4154
5217
  if (migrationChecked) {
@@ -4156,14 +5219,14 @@ function migrateLegacyRegistryIfNeeded() {
4156
5219
  }
4157
5220
  migrationChecked = true;
4158
5221
  ensureRegistryDir();
4159
- if (!fs12.existsSync(LEGACY_REGISTRY_FILE)) {
5222
+ if (!fs16.existsSync(LEGACY_REGISTRY_FILE)) {
4160
5223
  return;
4161
5224
  }
4162
5225
  if (listEntryFiles().length > 0) {
4163
5226
  return;
4164
5227
  }
4165
5228
  try {
4166
- const raw = fs12.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
5229
+ const raw = fs16.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
4167
5230
  const data = JSON.parse(raw);
4168
5231
  const packs = Array.isArray(data?.packs) ? data.packs : [];
4169
5232
  for (const pack of packs) {
@@ -4176,7 +5239,7 @@ function migrateLegacyRegistryIfNeeded() {
4176
5239
  } catch {
4177
5240
  }
4178
5241
  }
4179
- fs12.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
5242
+ fs16.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
4180
5243
  } catch (err) {
4181
5244
  console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
4182
5245
  }
@@ -4229,13 +5292,13 @@ function deregister(dir, pid) {
4229
5292
  }
4230
5293
 
4231
5294
  // src/runtime/server.ts
4232
- var __dirname = path14.dirname(fileURLToPath2(import.meta.url));
5295
+ var __dirname = path18.dirname(fileURLToPath2(import.meta.url));
4233
5296
  async function startServer(options) {
4234
5297
  const {
4235
5298
  rootDir,
4236
5299
  host = process.env.HOST || "127.0.0.1",
4237
5300
  port = Number(process.env.PORT) || 26313,
4238
- daemonRun = false
5301
+ runtimeMode = process.env.SKILLPACK_RUNTIME_MODE === "embedded" ? "embedded" : "standalone"
4239
5302
  } = options;
4240
5303
  const dataConfig = configManager.load(rootDir);
4241
5304
  const apiKey = dataConfig.apiKey || "";
@@ -4245,8 +5308,8 @@ async function startServer(options) {
4245
5308
  const baseUrl = dataConfig.baseUrl?.trim() || void 0;
4246
5309
  const modelId = dataConfig.modelId?.trim() || (SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId);
4247
5310
  const apiProtocol = dataConfig.apiProtocol;
4248
- const packageRoot = path14.resolve(__dirname, "..");
4249
- const webDir = fs15.existsSync(path14.join(rootDir, "web")) ? path14.join(rootDir, "web") : path14.join(packageRoot, "web");
5311
+ const packageRoot = path18.resolve(__dirname, "..");
5312
+ const webDir = fs19.existsSync(path18.join(rootDir, "web")) ? path18.join(rootDir, "web") : path18.join(packageRoot, "web");
4250
5313
  const app = express();
4251
5314
  app.use(express.json());
4252
5315
  app.use(express.static(webDir));
@@ -4264,6 +5327,13 @@ async function startServer(options) {
4264
5327
  });
4265
5328
  });
4266
5329
  const lifecycle = new Lifecycle(server);
5330
+ const resultStore = new ResultStore(rootDir);
5331
+ const artifactSnapshotService = new ArtifactSnapshotService(rootDir);
5332
+ const artifactPersistenceService = new ArtifactPersistenceService(
5333
+ artifactSnapshotService,
5334
+ resultStore
5335
+ );
5336
+ const resultsQueryService = new ResultsQueryService(resultStore);
4267
5337
  const agent = new PackAgent({
4268
5338
  apiKey,
4269
5339
  rootDir,
@@ -4271,12 +5341,14 @@ async function startServer(options) {
4271
5341
  modelId,
4272
5342
  baseUrl,
4273
5343
  apiProtocol,
4274
- lifecycleHandler: lifecycle
5344
+ lifecycleHandler: lifecycle,
5345
+ artifactPersistenceService
4275
5346
  });
4276
5347
  const adapters = [];
4277
5348
  const adapterMap = /* @__PURE__ */ new Map();
4278
5349
  const hasIpcChannel = typeof process.send === "function";
4279
5350
  const ipcAdapter = new IpcAdapter();
5351
+ const webEnabled = runtimeMode === "standalone";
4280
5352
  if (hasIpcChannel) {
4281
5353
  await ipcAdapter.start({
4282
5354
  agent,
@@ -4284,24 +5356,28 @@ async function startServer(options) {
4284
5356
  app,
4285
5357
  rootDir,
4286
5358
  lifecycle,
4287
- adapterMap
5359
+ adapterMap,
5360
+ resultsQueryService
4288
5361
  });
4289
5362
  adapters.push(ipcAdapter);
4290
5363
  adapterMap.set(ipcAdapter.name, ipcAdapter);
4291
5364
  }
4292
5365
  const ipcBroadcaster = hasIpcChannel ? ipcAdapter : void 0;
4293
- const webAdapter = new WebAdapter();
4294
- await webAdapter.start({
4295
- agent,
4296
- server,
4297
- app,
4298
- rootDir,
4299
- lifecycle,
4300
- adapterMap,
4301
- ipcBroadcaster
4302
- });
4303
- adapters.push(webAdapter);
4304
- adapterMap.set(webAdapter.name, webAdapter);
5366
+ if (webEnabled) {
5367
+ const webAdapter = new WebAdapter();
5368
+ await webAdapter.start({
5369
+ agent,
5370
+ server,
5371
+ app,
5372
+ rootDir,
5373
+ lifecycle,
5374
+ adapterMap,
5375
+ ipcBroadcaster,
5376
+ resultsQueryService
5377
+ });
5378
+ adapters.push(webAdapter);
5379
+ adapterMap.set(webAdapter.name, webAdapter);
5380
+ }
4305
5381
  if (dataConfig.adapters?.telegram?.token) {
4306
5382
  try {
4307
5383
  const { TelegramAdapter: TelegramAdapter2 } = await Promise.resolve().then(() => (init_telegram(), telegram_exports));
@@ -4315,7 +5391,8 @@ async function startServer(options) {
4315
5391
  rootDir,
4316
5392
  lifecycle,
4317
5393
  adapterMap,
4318
- ipcBroadcaster
5394
+ ipcBroadcaster,
5395
+ resultsQueryService
4319
5396
  });
4320
5397
  adapters.push(telegramAdapter);
4321
5398
  adapterMap.set(telegramAdapter.name, telegramAdapter);
@@ -4343,7 +5420,8 @@ async function startServer(options) {
4343
5420
  rootDir,
4344
5421
  lifecycle,
4345
5422
  adapterMap,
4346
- ipcBroadcaster
5423
+ ipcBroadcaster,
5424
+ resultsQueryService
4347
5425
  });
4348
5426
  adapters.push(slackAdapter);
4349
5427
  adapterMap.set(slackAdapter.name, slackAdapter);
@@ -4363,7 +5441,6 @@ async function startServer(options) {
4363
5441
  }
4364
5442
  await adapter.sendMessage(channelId, text);
4365
5443
  };
4366
- const scheduledJobs = dataConfig.scheduledJobs || [];
4367
5444
  let schedulerAdapter = null;
4368
5445
  try {
4369
5446
  const { SchedulerAdapter: SchedulerAdapter2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
@@ -4375,13 +5452,11 @@ async function startServer(options) {
4375
5452
  rootDir,
4376
5453
  lifecycle,
4377
5454
  notify: notifyFn,
4378
- adapterMap
5455
+ adapterMap,
5456
+ resultsQueryService
4379
5457
  });
4380
5458
  adapters.push(schedulerAdapter);
4381
5459
  adapterMap.set(schedulerAdapter.name, schedulerAdapter);
4382
- if (scheduledJobs.length > 0) {
4383
- console.log(`[Server] Scheduler started with ${scheduledJobs.length} job(s)`);
4384
- }
4385
5460
  } catch (err) {
4386
5461
  console.error("[Scheduler] Failed to start:", err);
4387
5462
  }
@@ -4389,34 +5464,35 @@ async function startServer(options) {
4389
5464
  agent.setScheduler(schedulerAdapter);
4390
5465
  }
4391
5466
  lifecycle.registerAdapters(adapters);
4392
- server.once("listening", () => {
4393
- const address = server.address();
4394
- const actualPort = typeof address === "string" ? address : address?.port;
4395
- const url = `http://${host}:${actualPort}`;
4396
- console.log(`
5467
+ const announceReady = (actualPort) => {
5468
+ if (webEnabled) {
5469
+ const url = `http://${host}:${actualPort}`;
5470
+ console.log(`
4397
5471
  Skills Pack Server`);
4398
- console.log(` Running at ${url}
5472
+ console.log(` Running at ${url}
4399
5473
  `);
4400
- try {
4401
- register({
4402
- dir: canonicalRootDir,
4403
- name: packConfig.name,
4404
- version: packConfig.version,
4405
- port: typeof actualPort === "number" ? actualPort : port
4406
- });
4407
- } catch (err) {
4408
- console.warn(" [Registry] Could not register pack:", err);
4409
- }
4410
- if (hasIpcChannel) {
4411
- ipcAdapter.notifyReady(typeof actualPort === "number" ? actualPort : port);
4412
- }
4413
- if (!daemonRun) {
5474
+ try {
5475
+ register({
5476
+ dir: canonicalRootDir,
5477
+ name: packConfig.name,
5478
+ version: packConfig.version,
5479
+ port: actualPort
5480
+ });
5481
+ } catch (err) {
5482
+ console.warn(" [Registry] Could not register pack:", err);
5483
+ }
4414
5484
  const cmd = process.platform === "darwin" ? `open ${url}` : process.platform === "win32" ? `start ${url}` : `xdg-open ${url}`;
4415
5485
  exec(cmd, (err) => {
4416
5486
  if (err) console.warn(` Could not open browser: ${err.message}`);
4417
5487
  });
5488
+ } else {
5489
+ console.log("\n Skills Pack Server");
5490
+ console.log(" Running in embedded mode (IPC only)\n");
4418
5491
  }
4419
- });
5492
+ if (hasIpcChannel) {
5493
+ ipcAdapter.notifyReady(actualPort);
5494
+ }
5495
+ };
4420
5496
  process.on("SIGINT", () => {
4421
5497
  deregister(canonicalRootDir, process.pid);
4422
5498
  void lifecycle.requestShutdown("signal");
@@ -4425,22 +5501,30 @@ async function startServer(options) {
4425
5501
  deregister(canonicalRootDir, process.pid);
4426
5502
  void lifecycle.requestShutdown("signal");
4427
5503
  });
4428
- await new Promise((resolve, reject) => {
4429
- function tryListen(listenPort) {
4430
- server.listen(listenPort, host);
4431
- server.once("error", (err) => {
4432
- if (err.code === "EADDRINUSE") {
4433
- console.log(` Port ${listenPort} is in use, trying ${listenPort + 1}...`);
4434
- server.close();
4435
- tryListen(listenPort + 1);
4436
- } else {
4437
- reject(err);
4438
- }
4439
- });
4440
- server.once("listening", () => resolve());
4441
- }
4442
- tryListen(port);
4443
- });
5504
+ if (webEnabled) {
5505
+ const actualPort = await new Promise((resolve, reject) => {
5506
+ function tryListen(listenPort) {
5507
+ server.listen(listenPort, host);
5508
+ server.once("error", (err) => {
5509
+ if (err.code === "EADDRINUSE") {
5510
+ console.log(` Port ${listenPort} is in use, trying ${listenPort + 1}...`);
5511
+ server.close();
5512
+ tryListen(listenPort + 1);
5513
+ } else {
5514
+ reject(err);
5515
+ }
5516
+ });
5517
+ server.once("listening", () => {
5518
+ const address = server.address();
5519
+ resolve(typeof address === "string" ? listenPort : address?.port ?? listenPort);
5520
+ });
5521
+ }
5522
+ tryListen(port);
5523
+ });
5524
+ announceReady(actualPort);
5525
+ } else {
5526
+ announceReady(0);
5527
+ }
4444
5528
  await new Promise(() => {
4445
5529
  });
4446
5530
  }
@@ -4457,23 +5541,23 @@ function findMissingSkills(workDir, config) {
4457
5541
  });
4458
5542
  }
4459
5543
  function copyStartTemplates2(workDir) {
4460
- const templateDir = path15.resolve(
5544
+ const templateDir = path19.resolve(
4461
5545
  new URL("../templates", import.meta.url).pathname
4462
5546
  );
4463
5547
  for (const file of ["start.sh", "start.bat"]) {
4464
- const src = path15.join(templateDir, file);
4465
- const dest = path15.join(workDir, file);
4466
- if (fs16.existsSync(src)) {
4467
- fs16.copyFileSync(src, dest);
5548
+ const src = path19.join(templateDir, file);
5549
+ const dest = path19.join(workDir, file);
5550
+ if (fs20.existsSync(src)) {
5551
+ fs20.copyFileSync(src, dest);
4468
5552
  if (file === "start.sh") {
4469
- fs16.chmodSync(dest, 493);
5553
+ fs20.chmodSync(dest, 493);
4470
5554
  }
4471
5555
  }
4472
5556
  }
4473
5557
  }
4474
5558
  async function runCommand(directory) {
4475
- const workDir = directory ? path15.resolve(directory) : process.cwd();
4476
- fs16.mkdirSync(workDir, { recursive: true });
5559
+ const workDir = directory ? path19.resolve(directory) : process.cwd();
5560
+ fs20.mkdirSync(workDir, { recursive: true });
4477
5561
  if (!configExists(workDir)) {
4478
5562
  console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
4479
5563
  const { name, description } = await inquirer2.prompt([
@@ -4512,20 +5596,19 @@ async function runCommand(directory) {
4512
5596
  syncSkillDescriptions(workDir, config);
4513
5597
  saveConfig(workDir, config);
4514
5598
  await startServer({
4515
- rootDir: workDir,
4516
- daemonRun: process.env.DAEMON_RUN === "1"
5599
+ rootDir: workDir
4517
5600
  });
4518
5601
  }
4519
5602
 
4520
5603
  // src/cli.ts
4521
- import fs17 from "fs";
4522
- import path16 from "path";
5604
+ import fs21 from "fs";
5605
+ import path20 from "path";
4523
5606
  import { fileURLToPath as fileURLToPath3 } from "url";
4524
5607
  var packageJson = JSON.parse(
4525
- fs17.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
5608
+ fs21.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
4526
5609
  );
4527
5610
  var program = new Command();
4528
- var cliFilePath = path16.resolve(fileURLToPath3(import.meta.url));
5611
+ var cliFilePath = path20.resolve(fileURLToPath3(import.meta.url));
4529
5612
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
4530
5613
  program.command("create [directory]").description("Create a skills pack interactively").option("--config <path-or-url>", "Initialize from a local or remote skillpack.json").action(async (directory, options) => {
4531
5614
  await createCommand(directory, options);
@@ -4535,7 +5618,7 @@ program.command("run [directory]").description("Start the SkillPack runtime serv
4535
5618
  if (options?.host) process.env.HOST = options.host;
4536
5619
  await runCommand(directory);
4537
5620
  });
4538
- program.command("zip").description("Package the current pack as a zip file (skillpack.json + skills/ + start scripts)").action(async () => {
5621
+ program.command("zip").description("Package the current pack as a zip file (skillpack.json + optional job.json + skills/ + start scripts)").action(async () => {
4539
5622
  try {
4540
5623
  await zipCommand(process.cwd());
4541
5624
  } catch (err) {
@@ -4546,7 +5629,7 @@ program.command("zip").description("Package the current pack as a zip file (skil
4546
5629
  function normalizeUserArgs(argv) {
4547
5630
  if (argv.length === 0) return argv;
4548
5631
  const firstArg = argv[0];
4549
- if (firstArg && path16.resolve(firstArg) === cliFilePath) {
5632
+ if (firstArg && path20.resolve(firstArg) === cliFilePath) {
4550
5633
  return argv.slice(1);
4551
5634
  }
4552
5635
  return argv;