@cremini/skillpack 1.2.7 → 1.2.8

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
+ 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,381 @@ 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
+ 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
+ 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 Database from "better-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;
2913
+ constructor(rootDir) {
2914
+ const dataDir = path10.resolve(rootDir, "data");
2915
+ fs10.mkdirSync(dataDir, { recursive: true });
2916
+ this.db = new Database(path10.join(dataDir, "result.db"));
2917
+ this.db.pragma("journal_mode = WAL");
2918
+ this.initialize();
2919
+ }
2920
+ initialize() {
2921
+ this.db.exec(`
2922
+ CREATE TABLE IF NOT EXISTS artifacts (
2923
+ artifact_id TEXT PRIMARY KEY,
2924
+ run_id TEXT NOT NULL,
2925
+ channel_id TEXT NOT NULL,
2926
+ original_path TEXT NOT NULL,
2927
+ snapshot_path TEXT NOT NULL,
2928
+ file_name TEXT NOT NULL,
2929
+ mime_type TEXT,
2930
+ size_bytes INTEGER NOT NULL,
2931
+ title TEXT,
2932
+ is_primary INTEGER NOT NULL DEFAULT 0,
2933
+ declared_at TEXT NOT NULL
2934
+ );
2935
+
2936
+ CREATE INDEX IF NOT EXISTS idx_artifacts_channel_declared_at
2937
+ ON artifacts(channel_id, declared_at DESC);
2938
+ `);
2939
+ }
2940
+ insertArtifacts(input) {
2941
+ if (input.artifacts.length === 0) {
2942
+ return;
2943
+ }
2944
+ const insertArtifact = this.db.prepare(`
2945
+ INSERT INTO artifacts (
2946
+ artifact_id,
2947
+ run_id,
2948
+ channel_id,
2949
+ original_path,
2950
+ snapshot_path,
2951
+ file_name,
2952
+ mime_type,
2953
+ size_bytes,
2954
+ title,
2955
+ is_primary,
2956
+ declared_at
2957
+ ) VALUES (
2958
+ @artifactId,
2959
+ @runId,
2960
+ @channelId,
2961
+ @originalPath,
2962
+ @snapshotPath,
2963
+ @fileName,
2964
+ @mimeType,
2965
+ @sizeBytes,
2966
+ @title,
2967
+ @isPrimary,
2968
+ @declaredAt
2969
+ )
2970
+ `);
2971
+ const transaction = this.db.transaction((payload) => {
2972
+ for (const artifact of payload.artifacts) {
2973
+ insertArtifact.run({
2974
+ artifactId: randomUUID2(),
2975
+ runId: payload.runId,
2976
+ channelId: payload.channelId,
2977
+ originalPath: artifact.originalPath,
2978
+ snapshotPath: artifact.snapshotPath,
2979
+ fileName: artifact.fileName,
2980
+ mimeType: artifact.mimeType ?? null,
2981
+ sizeBytes: artifact.sizeBytes,
2982
+ title: artifact.title ?? null,
2983
+ isPrimary: artifact.isPrimary ? 1 : 0,
2984
+ declaredAt: artifact.declaredAt
2985
+ });
2986
+ }
2987
+ });
2988
+ transaction(input);
2989
+ }
2990
+ listRecentArtifacts(options = {}) {
2991
+ const limit = options.limit ?? 100;
2992
+ const offset = options.offset ?? 0;
2993
+ const conditions = [];
2994
+ const params = [];
2995
+ if (options.channelId) {
2996
+ conditions.push("channel_id = ?");
2997
+ params.push(options.channelId);
2998
+ }
2999
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3000
+ const rows = this.db.prepare(`
3001
+ SELECT *
3002
+ FROM artifacts
3003
+ ${whereClause}
3004
+ ORDER BY declared_at DESC, rowid DESC
3005
+ LIMIT ? OFFSET ?
3006
+ `).all(...params, limit, offset);
3007
+ return rows.map(mapArtifactRow);
3008
+ }
3009
+ };
3010
+
3011
+ // src/runtime/artifacts/save-artifacts-tool.ts
3012
+ import { Type } from "@sinclair/typebox";
3013
+ var ArtifactItem = Type.Object({
3014
+ filePath: Type.String({
3015
+ description: "Absolute path to the artifact file. The file must exist, be readable, and be inside the current pack root."
3016
+ }),
3017
+ title: Type.Optional(
3018
+ Type.String({
3019
+ description: "Optional short title shown in the dashboard."
3020
+ })
3021
+ ),
3022
+ isPrimary: Type.Optional(
3023
+ Type.Boolean({
3024
+ description: "Mark this artifact as a primary output."
3025
+ })
3026
+ )
3027
+ });
3028
+ var SaveArtifactsParams = Type.Object({
3029
+ artifacts: Type.Array(ArtifactItem, {
3030
+ minItems: 1,
3031
+ description: "The artifact files to save for this run."
3032
+ })
3033
+ });
3034
+ function textResult(text) {
3035
+ return { content: [{ type: "text", text }], details: void 0 };
3036
+ }
3037
+ function normalizeOptionalText(value) {
3038
+ const trimmed = value?.trim();
3039
+ return trimmed ? trimmed : void 0;
3040
+ }
3041
+ function createSaveArtifactsTool(rootDir, saveCallbackRef) {
3042
+ return {
3043
+ name: "save_artifacts",
3044
+ label: "Save Artifacts",
3045
+ description: [
3046
+ "Save the final output files produced by this run.",
3047
+ "Always use this for user-facing deliverables that are part of the final result.",
3048
+ "Do not use this for intermediate, temporary, draft, or scratch files.",
3049
+ "Each filePath must be an absolute path inside the current pack root."
3050
+ ].join("\n"),
3051
+ 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.",
3052
+ promptGuidelines: [
3053
+ "Whenever you create a final result file for the user, call `save_artifacts` before finishing the response.",
3054
+ "Do not call `save_artifacts` for intermediate, temporary, draft, or scratch files."
3055
+ ],
3056
+ parameters: SaveArtifactsParams,
3057
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
3058
+ const saveArtifacts = saveCallbackRef.current;
3059
+ if (!saveArtifacts) {
3060
+ throw new Error("Artifact saving is not available for this run.");
3061
+ }
3062
+ const artifacts = params.artifacts.map((artifact) => {
3063
+ const metadata = resolvePackFile(rootDir, artifact.filePath);
3064
+ return {
3065
+ filePath: metadata.resolvedPath,
3066
+ fileName: metadata.fileName,
3067
+ mimeType: metadata.mimeType,
3068
+ sizeBytes: metadata.sizeBytes,
3069
+ title: normalizeOptionalText(artifact.title),
3070
+ isPrimary: artifact.isPrimary === true
3071
+ };
3072
+ });
3073
+ const savedCount = await saveArtifacts(artifacts);
3074
+ return textResult(`Saved ${savedCount} artifact(s).`);
3075
+ }
3076
+ };
3077
+ }
3078
+
3079
+ // src/runtime/tools/send-file-tool.ts
3080
+ import fs11 from "fs";
3081
+ import path11 from "path";
3082
+ import { Type as Type2 } from "@sinclair/typebox";
3083
+ var SendFileParams = Type2.Object({
3084
+ filePath: Type2.String({
3085
+ description: "Absolute path to the file to send to the user. The file must exist and be readable."
3086
+ }),
3087
+ caption: Type2.Optional(
3088
+ Type2.String({
3089
+ description: "Optional caption or description to accompany the file."
3090
+ })
3091
+ )
3092
+ });
3093
+ function createSendFileTool(fileOutputCallbackRef) {
3094
+ return {
3095
+ name: "send_file",
3096
+ label: "Send File",
3097
+ 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.",
3098
+ promptSnippet: "send_file: Send a file to the user ONLY when they explicitly request it. Never send files proactively or automatically.",
3099
+ parameters: SendFileParams,
3100
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
3101
+ const { filePath, caption } = params;
3102
+ if (!fs11.existsSync(filePath)) {
3103
+ return {
3104
+ content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
3105
+ details: void 0
3106
+ };
3107
+ }
3108
+ const stats = fs11.statSync(filePath);
3109
+ if (!stats.isFile()) {
2555
3110
  return {
2556
3111
  content: [
2557
3112
  { type: "text", text: `Error: Path is not a file: ${filePath}` }
@@ -2559,7 +3114,7 @@ function createSendFileTool(fileOutputCallbackRef) {
2559
3114
  details: void 0
2560
3115
  };
2561
3116
  }
2562
- const filename = path7.basename(filePath);
3117
+ const filename = path11.basename(filePath);
2563
3118
  const mimeType = detectMimeType(filePath);
2564
3119
  const callback = fileOutputCallbackRef.current;
2565
3120
  if (callback) {
@@ -2586,43 +3141,65 @@ function createSendFileTool(fileOutputCallbackRef) {
2586
3141
  }
2587
3142
 
2588
3143
  // src/runtime/tools/manage-schedule-tool.ts
2589
- import { Type as Type2 } from "@sinclair/typebox";
2590
- var ManageScheduleParams = Type2.Object({
2591
- action: Type2.Union(
3144
+ import { Type as Type3 } from "@sinclair/typebox";
3145
+ var ManageScheduleParams = Type3.Object({
3146
+ action: Type3.Union(
2592
3147
  [
2593
- Type2.Literal("add"),
2594
- Type2.Literal("list"),
2595
- Type2.Literal("remove"),
2596
- Type2.Literal("trigger"),
2597
- Type2.Literal("enable"),
2598
- Type2.Literal("disable")
3148
+ Type3.Literal("add"),
3149
+ Type3.Literal("list"),
3150
+ Type3.Literal("remove"),
3151
+ Type3.Literal("trigger"),
3152
+ Type3.Literal("enable"),
3153
+ Type3.Literal("disable")
2599
3154
  ],
2600
3155
  { description: "The action to perform." }
2601
3156
  ),
2602
- name: Type2.Optional(
2603
- Type2.String({
3157
+ name: Type3.Optional(
3158
+ Type3.String({
2604
3159
  description: "Unique name for the scheduled task. Required for add/remove/trigger/enable/disable."
2605
3160
  })
2606
3161
  ),
2607
- cron: Type2.Optional(
2608
- Type2.String({
3162
+ cron: Type3.Optional(
3163
+ Type3.String({
2609
3164
  description: "Cron expression (5 fields: minute hour day month weekday). Required for add."
2610
3165
  })
2611
3166
  ),
2612
- prompt: Type2.Optional(
2613
- Type2.String({
3167
+ prompt: Type3.Optional(
3168
+ Type3.String({
2614
3169
  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
3170
  })
2616
3171
  ),
2617
- timezone: Type2.Optional(
2618
- Type2.String({
3172
+ timezone: Type3.Optional(
3173
+ Type3.String({
2619
3174
  description: "Optional timezone for the cron schedule, e.g. 'Asia/Shanghai', 'America/New_York'."
2620
3175
  })
3176
+ ),
3177
+ notifyAdapter: Type3.Optional(
3178
+ Type3.String({
3179
+ description: "Optional target adapter for notifications. If omitted, the current chat is used when supported (Telegram, Slack, or Web)."
3180
+ })
3181
+ ),
3182
+ notifyChannelId: Type3.Optional(
3183
+ Type3.String({
3184
+ description: "Optional target channelId for notifications. Must be provided together with notifyAdapter when overriding the default target."
3185
+ })
2621
3186
  )
2622
3187
  });
2623
- function textResult(text) {
3188
+ function textResult2(text) {
2624
3189
  return { content: [{ type: "text", text }], details: void 0 };
2625
3190
  }
3191
+ function getDefaultNotifyTarget(adapter, channelId) {
3192
+ if (adapter === "telegram" && channelId.startsWith("telegram-")) {
3193
+ return { adapter: "telegram", channelId };
3194
+ }
3195
+ if (adapter === "slack" && channelId.startsWith("slack-")) {
3196
+ return { adapter: "slack", channelId };
3197
+ }
3198
+ if (adapter === "web") {
3199
+ return { adapter: "web", channelId };
3200
+ }
3201
+ return null;
3202
+ }
2626
3203
  function createManageScheduleTool(schedulerRef, adapter, channelId) {
2627
3204
  return {
2628
3205
  name: "manage_scheduled_task",
@@ -2631,7 +3208,7 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
2631
3208
  "Manage scheduled tasks (cron jobs) that automatically execute prompts and push results to IM channels.",
2632
3209
  "",
2633
3210
  "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.",
3211
+ "- 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
3212
  "- list: List all scheduled tasks with their status.",
2636
3213
  "- remove: Remove a scheduled task by name.",
2637
3214
  "- trigger: Manually trigger a scheduled task by name (runs immediately).",
@@ -2648,7 +3225,7 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
2648
3225
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
2649
3226
  const scheduler = schedulerRef.current;
2650
3227
  if (!scheduler) {
2651
- return textResult(
3228
+ return textResult2(
2652
3229
  "Error: Scheduler is not available. The scheduled task system may not be initialized."
2653
3230
  );
2654
3231
  }
@@ -2656,79 +3233,85 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
2656
3233
  case "list": {
2657
3234
  const jobs = scheduler.listJobs();
2658
3235
  if (jobs.length === 0) {
2659
- return textResult("No scheduled tasks configured.");
3236
+ return textResult2("No scheduled tasks configured.");
2660
3237
  }
2661
3238
  const lines = jobs.map(
2662
3239
  (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
3240
  );
2664
- return textResult(
3241
+ return textResult2(
2665
3242
  `Scheduled tasks (${jobs.length}):
2666
3243
  ${lines.join("\n")}`
2667
3244
  );
2668
3245
  }
2669
3246
  case "add": {
2670
3247
  if (!params.name || !params.cron || !params.prompt) {
2671
- return textResult(
3248
+ return textResult2(
2672
3249
  "Error: 'name', 'cron', and 'prompt' are required for adding a task."
2673
3250
  );
2674
3251
  }
2675
- if (adapter !== "telegram" && adapter !== "slack") {
2676
- return textResult(
2677
- "Error: Scheduled tasks can only be created from a Telegram or Slack."
3252
+ if (params.notifyAdapter && !params.notifyChannelId || !params.notifyAdapter && params.notifyChannelId) {
3253
+ return textResult2(
3254
+ "Error: 'notifyAdapter' and 'notifyChannelId' must be provided together when overriding the notification target."
3255
+ );
3256
+ }
3257
+ const notify = params.notifyAdapter && params.notifyChannelId ? {
3258
+ adapter: params.notifyAdapter,
3259
+ channelId: params.notifyChannelId
3260
+ } : getDefaultNotifyTarget(adapter, channelId);
3261
+ if (!notify) {
3262
+ return textResult2(
3263
+ "Error: No default notification target is available for this chat. Provide 'notifyAdapter' and 'notifyChannelId'."
2678
3264
  );
2679
3265
  }
2680
3266
  const jobConfig = {
2681
3267
  name: params.name,
2682
3268
  cron: params.cron,
2683
3269
  prompt: params.prompt,
2684
- notify: {
2685
- adapter,
2686
- channelId
2687
- },
3270
+ notify,
2688
3271
  enabled: true,
2689
3272
  timezone: params.timezone
2690
3273
  };
2691
3274
  const result = scheduler.addJob(jobConfig);
2692
- return textResult(result.message);
3275
+ return textResult2(result.message);
2693
3276
  }
2694
3277
  case "remove": {
2695
3278
  if (!params.name) {
2696
- return textResult(
3279
+ return textResult2(
2697
3280
  "Error: 'name' is required for removing a task."
2698
3281
  );
2699
3282
  }
2700
3283
  const result = scheduler.removeJob(params.name);
2701
- return textResult(result.message);
3284
+ return textResult2(result.message);
2702
3285
  }
2703
3286
  case "trigger": {
2704
3287
  if (!params.name) {
2705
- return textResult(
3288
+ return textResult2(
2706
3289
  "Error: 'name' is required for triggering a task."
2707
3290
  );
2708
3291
  }
2709
3292
  const result = await scheduler.triggerJob(params.name);
2710
- return textResult(result.message);
3293
+ return textResult2(result.message);
2711
3294
  }
2712
3295
  case "enable": {
2713
3296
  if (!params.name) {
2714
- return textResult(
3297
+ return textResult2(
2715
3298
  "Error: 'name' is required for enabling a task."
2716
3299
  );
2717
3300
  }
2718
3301
  const result = scheduler.setEnabled(params.name, true);
2719
- return textResult(result.message);
3302
+ return textResult2(result.message);
2720
3303
  }
2721
3304
  case "disable": {
2722
3305
  if (!params.name) {
2723
- return textResult(
3306
+ return textResult2(
2724
3307
  "Error: 'name' is required for disabling a task."
2725
3308
  );
2726
3309
  }
2727
3310
  const result = scheduler.setEnabled(params.name, false);
2728
- return textResult(result.message);
3311
+ return textResult2(result.message);
2729
3312
  }
2730
3313
  default:
2731
- return textResult(
3314
+ return textResult2(
2732
3315
  `Error: Unknown action '${params.action}'. Use: add, list, remove, trigger, enable, disable.`
2733
3316
  );
2734
3317
  }
@@ -2738,8 +3321,8 @@ ${lines.join("\n")}`
2738
3321
 
2739
3322
  // src/runtime/commands/help-command.ts
2740
3323
  init_commands();
2741
- import fs8 from "fs";
2742
- import path8 from "path";
3324
+ import fs12 from "fs";
3325
+ import path12 from "path";
2743
3326
  function buildHelpMessage(rootDir) {
2744
3327
  const sections = [];
2745
3328
  const commands = getVisibleCommands();
@@ -2749,7 +3332,7 @@ function buildHelpMessage(rootDir) {
2749
3332
  sections.push(`\u{1F4CB} **Available Commands**
2750
3333
 
2751
3334
  ${commandLines.join("\n")}`);
2752
- const configPath = path8.resolve(rootDir, "skillpack.json");
3335
+ const configPath = path12.resolve(rootDir, "skillpack.json");
2753
3336
  const skills = readInstalledSkills(configPath);
2754
3337
  if (skills.length > 0) {
2755
3338
  const skillLines = skills.map(
@@ -2785,11 +3368,11 @@ function handleHelpCommand(rootDir) {
2785
3368
  };
2786
3369
  }
2787
3370
  function readInstalledSkills(configPath) {
2788
- if (!fs8.existsSync(configPath)) {
3371
+ if (!fs12.existsSync(configPath)) {
2789
3372
  return [];
2790
3373
  }
2791
3374
  try {
2792
- const raw = fs8.readFileSync(configPath, "utf-8");
3375
+ const raw = fs12.readFileSync(configPath, "utf-8");
2793
3376
  const config = JSON.parse(raw);
2794
3377
  return Array.isArray(config.skills) ? config.skills : [];
2795
3378
  } catch {
@@ -2809,24 +3392,24 @@ var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
2809
3392
  var PACK_AGENTS_FILE = "AGENTS.md";
2810
3393
  var PACK_SOUL_FILE = "SOUL.md";
2811
3394
  function materializeBuiltinSkillCreator(rootDir, skillsPath) {
2812
- if (!fs9.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
3395
+ if (!fs13.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
2813
3396
  log(
2814
3397
  `[PackAgent] Built-in skill-creator template missing: ${BUILTIN_SKILL_CREATOR_TEMPLATE_DIR}`
2815
3398
  );
2816
3399
  return null;
2817
3400
  }
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");
3401
+ const packConfigPath = path13.resolve(rootDir, "skillpack.json");
3402
+ const skillDir = path13.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
3403
+ const skillPath = path13.join(skillDir, "SKILL.md");
2821
3404
  const renderTemplate = (content) => content.replaceAll("{{SKILLS_PATH}}", skillsPath).replaceAll("{{PACK_CONFIG_PATH}}", packConfigPath);
2822
3405
  const copyDir = (srcDir, destDir) => {
2823
- fs9.mkdirSync(destDir, { recursive: true });
2824
- for (const entry of fs9.readdirSync(srcDir, { withFileTypes: true })) {
3406
+ fs13.mkdirSync(destDir, { recursive: true });
3407
+ for (const entry of fs13.readdirSync(srcDir, { withFileTypes: true })) {
2825
3408
  if (entry.name === ".DS_Store") {
2826
3409
  continue;
2827
3410
  }
2828
- const srcPath = path9.join(srcDir, entry.name);
2829
- const destPath = path9.join(destDir, entry.name);
3411
+ const srcPath = path13.join(srcDir, entry.name);
3412
+ const destPath = path13.join(destDir, entry.name);
2830
3413
  if (entry.isDirectory()) {
2831
3414
  copyDir(srcPath, destPath);
2832
3415
  continue;
@@ -2835,17 +3418,17 @@ function materializeBuiltinSkillCreator(rootDir, skillsPath) {
2835
3418
  continue;
2836
3419
  }
2837
3420
  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");
3421
+ const content = fs13.readFileSync(srcPath, "utf-8");
3422
+ fs13.writeFileSync(destPath, renderTemplate(content), "utf-8");
2840
3423
  continue;
2841
3424
  }
2842
- fs9.copyFileSync(srcPath, destPath);
3425
+ fs13.copyFileSync(srcPath, destPath);
2843
3426
  }
2844
3427
  };
2845
- if (!fs9.existsSync(skillDir)) {
3428
+ if (!fs13.existsSync(skillDir)) {
2846
3429
  copyDir(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR, skillDir);
2847
3430
  }
2848
- if (!fs9.existsSync(skillPath)) {
3431
+ if (!fs13.existsSync(skillPath)) {
2849
3432
  log(
2850
3433
  `[PackAgent] Materialized built-in skill-creator but SKILL.md is missing: ${skillPath}`
2851
3434
  );
@@ -2873,11 +3456,11 @@ function overrideBuiltinSkillCreator(base, materializedSkill) {
2873
3456
  };
2874
3457
  }
2875
3458
  function readOptionalPackPromptFile(filePath) {
2876
- if (!fs9.existsSync(filePath)) {
3459
+ if (!fs13.existsSync(filePath)) {
2877
3460
  return void 0;
2878
3461
  }
2879
3462
  try {
2880
- const content = fs9.readFileSync(filePath, "utf-8").trim();
3463
+ const content = fs13.readFileSync(filePath, "utf-8").trim();
2881
3464
  return content.length > 0 ? content : void 0;
2882
3465
  } catch (error) {
2883
3466
  console.warn(`[PackAgent] Warning: Could not read ${filePath}:`, error);
@@ -2885,8 +3468,8 @@ function readOptionalPackPromptFile(filePath) {
2885
3468
  }
2886
3469
  }
2887
3470
  function buildPackPromptBlock(rootDir) {
2888
- const agentsPath = path9.resolve(rootDir, PACK_AGENTS_FILE);
2889
- const soulPath = path9.resolve(rootDir, PACK_SOUL_FILE);
3471
+ const agentsPath = path13.resolve(rootDir, PACK_AGENTS_FILE);
3472
+ const soulPath = path13.resolve(rootDir, PACK_SOUL_FILE);
2890
3473
  const agentsContent = readOptionalPackPromptFile(agentsPath);
2891
3474
  const soulContent = readOptionalPackPromptFile(soulPath);
2892
3475
  if (!agentsContent && !soulContent) {
@@ -2945,14 +3528,13 @@ var PackAgent = class {
2945
3528
  options;
2946
3529
  channels = /* @__PURE__ */ new Map();
2947
3530
  pendingSessionCreations = /* @__PURE__ */ new Map();
2948
- fileOutputCallbackRef = {
2949
- current: null
2950
- };
2951
3531
  schedulerRef = { current: null };
2952
3532
  authStorage;
3533
+ artifactPersistenceService;
2953
3534
  constructor(options) {
2954
3535
  this.options = options;
2955
- const configPath = path9.resolve(options.rootDir, "data", "config.json");
3536
+ this.artifactPersistenceService = options.artifactPersistenceService;
3537
+ const configPath = path13.resolve(options.rootDir, "data", "config.json");
2956
3538
  const backend = new ConfigFileAuthBackend(configPath);
2957
3539
  this.authStorage = AuthStorage.fromStorage(backend);
2958
3540
  const providerMeta = SUPPORTED_PROVIDERS[options.provider];
@@ -2979,9 +3561,12 @@ var PackAgent = class {
2979
3561
  setScheduler(scheduler) {
2980
3562
  this.schedulerRef.current = scheduler;
2981
3563
  }
2982
- createCustomTools(adapter, channelId) {
2983
- const tools = [createSendFileTool(this.fileOutputCallbackRef)];
2984
- if (adapter === "telegram" || adapter === "slack") {
3564
+ createCustomTools(adapter, channelId, fileOutputCallbackRef, finalArtifactsSaveCallbackRef) {
3565
+ const tools = [
3566
+ createSendFileTool(fileOutputCallbackRef),
3567
+ createSaveArtifactsTool(this.options.rootDir, finalArtifactsSaveCallbackRef)
3568
+ ];
3569
+ if (adapter !== "scheduler") {
2985
3570
  tools.push(createManageScheduleTool(this.schedulerRef, adapter, channelId));
2986
3571
  }
2987
3572
  return tools;
@@ -3023,24 +3608,24 @@ var PackAgent = class {
3023
3608
  if (resolvedModel && baseUrl) {
3024
3609
  log(`[PackAgent] Resolved ${provider}/${modelId} api=${resolvedModel.api} baseUrl=${baseUrl}`);
3025
3610
  }
3026
- const sessionDir = path9.resolve(
3611
+ const sessionDir = path13.resolve(
3027
3612
  rootDir,
3028
3613
  "data",
3029
3614
  "sessions",
3030
3615
  channelId
3031
3616
  );
3032
- fs9.mkdirSync(sessionDir, { recursive: true });
3617
+ fs13.mkdirSync(sessionDir, { recursive: true });
3033
3618
  const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
3034
3619
  log(`[PackAgent] Session dir: ${sessionDir}`);
3035
- const workspaceDir = path9.resolve(
3620
+ const workspaceDir = path13.resolve(
3036
3621
  rootDir,
3037
3622
  "data",
3038
3623
  "workspaces",
3039
3624
  channelId
3040
3625
  );
3041
- fs9.mkdirSync(workspaceDir, { recursive: true });
3626
+ fs13.mkdirSync(workspaceDir, { recursive: true });
3042
3627
  log(`[PackAgent] Workspace dir: ${workspaceDir}`);
3043
- const skillsPath = path9.resolve(rootDir, "skills");
3628
+ const skillsPath = path13.resolve(rootDir, "skills");
3044
3629
  log(`[PackAgent] Loading skills from: ${skillsPath}`);
3045
3630
  const materializedSkillCreator = materializeBuiltinSkillCreator(
3046
3631
  rootDir,
@@ -3075,7 +3660,18 @@ var PackAgent = class {
3075
3660
  });
3076
3661
  await resourceLoader.reload();
3077
3662
  const tools = createCodingTools(workspaceDir);
3078
- const customTools = this.createCustomTools(adapter, channelId);
3663
+ const fileOutputCallbackRef = {
3664
+ current: null
3665
+ };
3666
+ const finalArtifactsSaveCallbackRef = {
3667
+ current: null
3668
+ };
3669
+ const customTools = this.createCustomTools(
3670
+ adapter,
3671
+ channelId,
3672
+ fileOutputCallbackRef,
3673
+ finalArtifactsSaveCallbackRef
3674
+ );
3079
3675
  const { session } = await createAgentSession({
3080
3676
  cwd: workspaceDir,
3081
3677
  authStorage,
@@ -3089,7 +3685,9 @@ var PackAgent = class {
3089
3685
  const channelSession = {
3090
3686
  session,
3091
3687
  running: false,
3092
- pending: Promise.resolve()
3688
+ pending: Promise.resolve(),
3689
+ fileOutputCallbackRef,
3690
+ finalArtifactsSaveCallbackRef
3093
3691
  };
3094
3692
  this.channels.set(channelId, channelSession);
3095
3693
  return channelSession;
@@ -3106,86 +3704,97 @@ var PackAgent = class {
3106
3704
  const run = async () => {
3107
3705
  cs.running = true;
3108
3706
  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(`
3707
+ const runId = randomUUID3();
3708
+ let unsubscribe = () => void 0;
3709
+ try {
3710
+ cs.fileOutputCallbackRef.current = (event) => {
3711
+ onEvent(event);
3712
+ };
3713
+ cs.finalArtifactsSaveCallbackRef.current = (artifacts) => {
3714
+ return this.artifactPersistenceService.saveArtifacts({
3715
+ runId,
3716
+ channelId,
3717
+ artifacts
3718
+ });
3719
+ };
3720
+ unsubscribe = cs.session.subscribe((event) => {
3721
+ switch (event.type) {
3722
+ case "agent_start":
3723
+ log("\n=== [AGENT SESSION START] ===");
3724
+ log("System Prompt:\n", cs.session.systemPrompt);
3725
+ log("============================\n");
3726
+ onEvent({ type: "agent_start" });
3727
+ break;
3728
+ case "message_start":
3729
+ log(`
3122
3730
  --- [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") {
3731
+ if (event.message?.role === "user") {
3732
+ log(JSON.stringify(event.message.content, null, 2));
3733
+ }
3734
+ onEvent({ type: "message_start", role: event.message?.role ?? "" });
3735
+ break;
3736
+ case "message_update":
3737
+ if (event.assistantMessageEvent?.type === "text_delta") {
3738
+ turnHadVisibleOutput = true;
3739
+ write(event.assistantMessageEvent.delta);
3740
+ onEvent({
3741
+ type: "text_delta",
3742
+ delta: event.assistantMessageEvent.delta
3743
+ });
3744
+ } else if (event.assistantMessageEvent?.type === "thinking_delta") {
3745
+ turnHadVisibleOutput = true;
3746
+ onEvent({
3747
+ type: "thinking_delta",
3748
+ delta: event.assistantMessageEvent.delta
3749
+ });
3750
+ }
3751
+ break;
3752
+ case "message_end":
3753
+ log(`
3754
+ --- [Message End: ${event.message?.role}] ---`);
3755
+ if (event.message?.role === "assistant") {
3756
+ const diagnostics2 = getAssistantDiagnostics(event.message);
3757
+ if (diagnostics2) {
3758
+ log(
3759
+ `[Assistant Diagnostics] stopReason=${diagnostics2.stopReason} text=${diagnostics2.hasText ? "yes" : "no"} toolCalls=${diagnostics2.toolCalls}`
3760
+ );
3761
+ if (diagnostics2.errorMessage) {
3762
+ log(`[Assistant Error] ${diagnostics2.errorMessage}`);
3763
+ }
3764
+ }
3765
+ }
3766
+ onEvent({ type: "message_end", role: event.message?.role ?? "" });
3767
+ break;
3768
+ case "tool_execution_start":
3130
3769
  turnHadVisibleOutput = true;
3131
- write(event.assistantMessageEvent.delta);
3770
+ log(`
3771
+ >>> [Tool Start: ${event.toolName}] >>>`);
3772
+ log("Args:", JSON.stringify(event.args, null, 2));
3132
3773
  onEvent({
3133
- type: "text_delta",
3134
- delta: event.assistantMessageEvent.delta
3774
+ type: "tool_start",
3775
+ toolCallId: event.toolCallId ?? "",
3776
+ toolName: event.toolName,
3777
+ toolInput: event.args
3135
3778
  });
3136
- } else if (event.assistantMessageEvent?.type === "thinking_delta") {
3779
+ break;
3780
+ case "tool_execution_end":
3137
3781
  turnHadVisibleOutput = true;
3782
+ log(`<<< [Tool End: ${event.toolName}] <<<`);
3783
+ log(`Error: ${event.isError ? "Yes" : "No"}`);
3138
3784
  onEvent({
3139
- type: "thinking_delta",
3140
- delta: event.assistantMessageEvent.delta
3785
+ type: "tool_end",
3786
+ toolCallId: event.toolCallId ?? "",
3787
+ toolName: event.toolName,
3788
+ isError: event.isError,
3789
+ result: event.result
3141
3790
  });
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 {
3791
+ break;
3792
+ case "agent_end":
3793
+ log("\n=== [AGENT SESSION END] ===\n");
3794
+ onEvent({ type: "agent_end" });
3795
+ break;
3796
+ }
3797
+ });
3189
3798
  let promptText = text;
3190
3799
  const promptOptions = {};
3191
3800
  if (attachments && attachments.length > 0) {
@@ -3213,15 +3822,17 @@ ${text}`;
3213
3822
  };
3214
3823
  }
3215
3824
  if (diagnostics && !diagnostics.hasText && diagnostics.toolCalls === 0 && !turnHadVisibleOutput) {
3825
+ const errorMessage = "Assistant returned no visible output. Check the server logs for details.";
3216
3826
  return {
3217
3827
  stopReason: diagnostics.stopReason,
3218
- errorMessage: "Assistant returned no visible output. Check the server logs for details."
3828
+ errorMessage
3219
3829
  };
3220
3830
  }
3221
3831
  return { stopReason: diagnostics?.stopReason ?? "unknown" };
3222
3832
  } finally {
3223
3833
  cs.running = false;
3224
- this.fileOutputCallbackRef.current = null;
3834
+ cs.fileOutputCallbackRef.current = null;
3835
+ cs.finalArtifactsSaveCallbackRef.current = null;
3225
3836
  unsubscribe();
3226
3837
  }
3227
3838
  };
@@ -3241,9 +3852,9 @@ ${text}`;
3241
3852
  this.channels.delete(channelId);
3242
3853
  }
3243
3854
  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 });
3855
+ const sessionDir = path13.resolve(rootDir, "data", "sessions", channelId);
3856
+ if (fs13.existsSync(sessionDir)) {
3857
+ fs13.rmSync(sessionDir, { recursive: true, force: true });
3247
3858
  log(`[PackAgent] Cleared session dir: ${sessionDir}`);
3248
3859
  }
3249
3860
  return {
@@ -3265,42 +3876,437 @@ ${text}`;
3265
3876
  return { success: false, message: `Unknown command: ${command}` };
3266
3877
  }
3267
3878
  }
3268
- abort(channelId) {
3269
- const cs = this.channels.get(channelId);
3270
- if (cs?.running) {
3271
- cs.session.abort?.();
3879
+ abort(channelId) {
3880
+ const cs = this.channels.get(channelId);
3881
+ if (cs?.running) {
3882
+ cs.session.abort?.();
3883
+ }
3884
+ }
3885
+ isRunning(channelId) {
3886
+ return this.channels.get(channelId)?.running ?? false;
3887
+ }
3888
+ dispose(channelId) {
3889
+ const cs = this.channels.get(channelId);
3890
+ if (cs) {
3891
+ cs.session.dispose();
3892
+ this.channels.delete(channelId);
3893
+ }
3894
+ }
3895
+ /** Reserved: list all sessions */
3896
+ listSessions() {
3897
+ return [];
3898
+ }
3899
+ /** Reserved: restore a historical session */
3900
+ async restoreSession(_sessionId) {
3901
+ }
3902
+ getActiveChannelIds() {
3903
+ return Array.from(this.channels.keys());
3904
+ }
3905
+ };
3906
+
3907
+ // src/runtime/adapters/web.ts
3908
+ import fs15 from "fs";
3909
+ import path15 from "path";
3910
+ import { WebSocketServer } from "ws";
3911
+ init_commands();
3912
+
3913
+ // src/runtime/services/conversation.ts
3914
+ import fs14 from "fs";
3915
+ import path14 from "path";
3916
+ import {
3917
+ parseSessionEntries
3918
+ } from "@mariozechner/pi-coding-agent";
3919
+ var DEFAULT_WEB_CHANNEL_ID = "web";
3920
+ var ConversationService = class {
3921
+ constructor(rootDir) {
3922
+ this.rootDir = rootDir;
3923
+ }
3924
+ /**
3925
+ * Scan data/sessions and return conversation summaries sorted by recency.
3926
+ */
3927
+ listConversations(activeChannels, options = {}) {
3928
+ const {
3929
+ includeDefaultWeb = false,
3930
+ includeLegacyWeb = true,
3931
+ allowedPlatforms
3932
+ } = options;
3933
+ const sessionsDir = path14.resolve(this.rootDir, "data", "sessions");
3934
+ const channelIds = new Set(activeChannels);
3935
+ const allowedPlatformSet = allowedPlatforms ? new Set(allowedPlatforms) : null;
3936
+ if (includeDefaultWeb) {
3937
+ channelIds.add(DEFAULT_WEB_CHANNEL_ID);
3938
+ }
3939
+ if (fs14.existsSync(sessionsDir)) {
3940
+ for (const entry of fs14.readdirSync(sessionsDir)) {
3941
+ const channelDir = path14.join(sessionsDir, entry);
3942
+ try {
3943
+ if (!fs14.statSync(channelDir).isDirectory()) {
3944
+ continue;
3945
+ }
3946
+ const platform = this.detectPlatform(entry);
3947
+ if (allowedPlatformSet && !allowedPlatformSet.has(platform)) {
3948
+ continue;
3949
+ }
3950
+ if (!includeLegacyWeb && this.isLegacyWebConversation(entry)) {
3951
+ continue;
3952
+ }
3953
+ channelIds.add(entry);
3954
+ } catch {
3955
+ }
3956
+ }
3957
+ }
3958
+ const results = [];
3959
+ for (const channelId of channelIds) {
3960
+ const platform = this.detectPlatform(channelId);
3961
+ if (allowedPlatformSet && !allowedPlatformSet.has(platform)) {
3962
+ continue;
3963
+ }
3964
+ if (!includeLegacyWeb && this.isLegacyWebConversation(channelId)) {
3965
+ continue;
3966
+ }
3967
+ const channelDir = path14.join(sessionsDir, channelId);
3968
+ const sessionFile = this.findLatestSessionFile(channelDir);
3969
+ let messageCount = 0;
3970
+ let lastMessageAt = "";
3971
+ let lastMessagePreview = "";
3972
+ if (sessionFile) {
3973
+ const entries = this.loadEntries(sessionFile);
3974
+ const messages = entries.filter(
3975
+ (entry) => entry.type === "message"
3976
+ );
3977
+ messageCount = messages.length;
3978
+ const lastMessage = messages[messages.length - 1];
3979
+ if (lastMessage) {
3980
+ lastMessageAt = lastMessage.timestamp;
3981
+ lastMessagePreview = this.extractTextPreview(lastMessage, 100);
3982
+ }
3983
+ }
3984
+ results.push({
3985
+ channelId,
3986
+ platform,
3987
+ sessionFile,
3988
+ messageCount,
3989
+ lastMessageAt,
3990
+ lastMessagePreview
3991
+ });
3992
+ }
3993
+ return results.sort((a, b) => {
3994
+ if (a.channelId === DEFAULT_WEB_CHANNEL_ID && b.channelId !== DEFAULT_WEB_CHANNEL_ID) {
3995
+ return -1;
3996
+ }
3997
+ if (b.channelId === DEFAULT_WEB_CHANNEL_ID && a.channelId !== DEFAULT_WEB_CHANNEL_ID) {
3998
+ return 1;
3999
+ }
4000
+ const recency = (b.lastMessageAt || "").localeCompare(
4001
+ a.lastMessageAt || ""
4002
+ );
4003
+ if (recency !== 0) return recency;
4004
+ return a.channelId.localeCompare(b.channelId);
4005
+ });
4006
+ }
4007
+ /**
4008
+ * Load latest messages for a channel in a simplified format.
4009
+ */
4010
+ getMessages(channelId, limit = 100) {
4011
+ const channelDir = path14.resolve(
4012
+ this.rootDir,
4013
+ "data",
4014
+ "sessions",
4015
+ channelId
4016
+ );
4017
+ const sessionFile = this.findLatestSessionFile(channelDir);
4018
+ if (!sessionFile) return [];
4019
+ const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 100;
4020
+ if (safeLimit === 0) return [];
4021
+ const entries = this.loadEntries(sessionFile);
4022
+ const toolResultsById = this.collectToolResultStates(entries);
4023
+ const messages = [];
4024
+ let pendingBlocks = [];
4025
+ let pendingToolCalls = [];
4026
+ let pendingMessageId = "";
4027
+ let pendingTimestamp = "";
4028
+ const flushPendingAssistant = () => {
4029
+ if (pendingBlocks.length === 0 && pendingToolCalls.length === 0) {
4030
+ return;
4031
+ }
4032
+ messages.push({
4033
+ id: pendingMessageId || `assistant-${messages.length + 1}`,
4034
+ role: "assistant",
4035
+ text: "",
4036
+ timestamp: pendingTimestamp || (/* @__PURE__ */ new Date(0)).toISOString(),
4037
+ toolCalls: pendingToolCalls.length > 0 ? pendingToolCalls : void 0,
4038
+ blocks: pendingBlocks.length > 0 ? pendingBlocks : void 0
4039
+ });
4040
+ pendingBlocks = [];
4041
+ pendingToolCalls = [];
4042
+ pendingMessageId = "";
4043
+ pendingTimestamp = "";
4044
+ };
4045
+ for (const entry of entries) {
4046
+ if (entry.type !== "message") continue;
4047
+ const role = entry.message?.role;
4048
+ if (role === "user") {
4049
+ flushPendingAssistant();
4050
+ const text2 = this.extractText(entry.message);
4051
+ if (!text2) continue;
4052
+ messages.push({
4053
+ id: entry.id,
4054
+ role,
4055
+ text: text2,
4056
+ timestamp: entry.timestamp
4057
+ });
4058
+ continue;
4059
+ }
4060
+ if (role !== "assistant") continue;
4061
+ const text = this.extractText(entry.message);
4062
+ const toolCalls = this.extractToolCalls(entry.message, toolResultsById);
4063
+ const blocks = this.extractBlocks(
4064
+ entry.id,
4065
+ entry.message,
4066
+ toolResultsById
4067
+ );
4068
+ if (!text) {
4069
+ if (blocks.length > 0) {
4070
+ pendingBlocks = [...pendingBlocks, ...blocks];
4071
+ }
4072
+ if (toolCalls?.length) {
4073
+ pendingToolCalls = this.mergeToolCalls(pendingToolCalls, toolCalls);
4074
+ }
4075
+ if (!pendingMessageId) {
4076
+ pendingMessageId = entry.id;
4077
+ pendingTimestamp = entry.timestamp;
4078
+ }
4079
+ continue;
4080
+ }
4081
+ const mergedBlocks = pendingBlocks.length > 0 ? [...pendingBlocks, ...blocks] : blocks;
4082
+ const mergedToolCalls = this.mergeToolCalls(pendingToolCalls, toolCalls);
4083
+ messages.push({
4084
+ id: entry.id,
4085
+ role,
4086
+ text,
4087
+ timestamp: entry.timestamp,
4088
+ toolCalls: mergedToolCalls.length > 0 ? mergedToolCalls : void 0,
4089
+ blocks: mergedBlocks.length > 0 ? mergedBlocks : void 0
4090
+ });
4091
+ pendingBlocks = [];
4092
+ pendingToolCalls = [];
4093
+ pendingMessageId = "";
4094
+ pendingTimestamp = "";
4095
+ }
4096
+ flushPendingAssistant();
4097
+ return messages.slice(-safeLimit);
4098
+ }
4099
+ findLatestSessionFile(channelDir) {
4100
+ if (!fs14.existsSync(channelDir)) return null;
4101
+ let stats;
4102
+ try {
4103
+ stats = fs14.statSync(channelDir);
4104
+ } catch {
4105
+ return null;
4106
+ }
4107
+ if (!stats.isDirectory()) return null;
4108
+ const files = fs14.readdirSync(channelDir).filter((file) => file.endsWith(".jsonl")).sort((a, b) => b.localeCompare(a));
4109
+ return files[0] ? path14.join(channelDir, files[0]) : null;
4110
+ }
4111
+ loadEntries(filePath) {
4112
+ try {
4113
+ const content = fs14.readFileSync(filePath, "utf-8");
4114
+ const fileEntries = parseSessionEntries(content);
4115
+ return fileEntries.filter(
4116
+ (entry) => entry.type !== "session"
4117
+ );
4118
+ } catch (err) {
4119
+ console.warn(`[ConversationService] Failed to load ${filePath}:`, err);
4120
+ return [];
4121
+ }
4122
+ }
4123
+ extractText(message) {
4124
+ if (!message?.content) return "";
4125
+ if (typeof message.content === "string") return message.content.trim();
4126
+ if (!Array.isArray(message.content)) return "";
4127
+ return message.content.filter((item) => item?.type === "text").map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
4128
+ }
4129
+ extractTextPreview(entry, maxLen) {
4130
+ const text = this.extractText(entry.message);
4131
+ return text.length > maxLen ? `${text.slice(0, maxLen)}\u2026` : text;
4132
+ }
4133
+ collectToolResultStates(entries) {
4134
+ const toolResultsById = /* @__PURE__ */ new Map();
4135
+ for (const entry of entries) {
4136
+ if (entry.type !== "message") continue;
4137
+ if (entry.message?.role !== "toolResult") continue;
4138
+ if (typeof entry.message?.toolCallId !== "string" || !entry.message.toolCallId) {
4139
+ continue;
4140
+ }
4141
+ toolResultsById.set(entry.message.toolCallId, {
4142
+ isError: entry.message?.isError === true,
4143
+ result: this.extractToolResultValue(entry.message)
4144
+ });
4145
+ }
4146
+ return toolResultsById;
4147
+ }
4148
+ extractToolCalls(message, toolResultsById) {
4149
+ if (!Array.isArray(message?.content)) return void 0;
4150
+ const toolCalls = message.content.filter((item) => item?.type === "toolCall").map((item) => {
4151
+ const id = typeof item?.id === "string" && item.id ? item.id : "unknown";
4152
+ const name = typeof item?.name === "string" && item.name ? item.name : "unknown";
4153
+ const toolCall = {
4154
+ id,
4155
+ name,
4156
+ isError: toolResultsById.get(id)?.isError === true
4157
+ };
4158
+ if (name === "send_file") {
4159
+ const args = this.extractSendFileArguments(item?.arguments);
4160
+ if (args) {
4161
+ toolCall.arguments = args;
4162
+ }
4163
+ }
4164
+ return toolCall;
4165
+ });
4166
+ return toolCalls.length > 0 ? toolCalls : void 0;
4167
+ }
4168
+ extractBlocks(messageId, message, toolResultsById) {
4169
+ if (!Array.isArray(message?.content)) {
4170
+ return [];
4171
+ }
4172
+ const blocks = [];
4173
+ message.content.forEach((item, index) => {
4174
+ if (item?.type === "thinking") {
4175
+ const thinkingText = this.extractThinkingText(item);
4176
+ if (thinkingText) {
4177
+ blocks.push({
4178
+ id: `${messageId}-thinking-${index}`,
4179
+ type: "thinking",
4180
+ text: thinkingText
4181
+ });
4182
+ }
4183
+ return;
4184
+ }
4185
+ if (item?.type !== "toolCall") {
4186
+ return;
4187
+ }
4188
+ const toolCallId = typeof item?.id === "string" && item.id ? item.id : `${messageId}-tool-${index}`;
4189
+ const toolName = typeof item?.name === "string" && item.name ? item.name : "unknown";
4190
+ const toolResult = toolResultsById.get(toolCallId);
4191
+ const sendFileArgs = toolName === "send_file" ? this.extractSendFileArguments(item?.arguments) : void 0;
4192
+ if (toolName === "send_file" && !toolResult?.isError && sendFileArgs?.filePath) {
4193
+ blocks.push({
4194
+ id: toolCallId,
4195
+ type: "file",
4196
+ filename: this.getFileBaseName(sendFileArgs.filePath),
4197
+ filePath: sendFileArgs.filePath,
4198
+ caption: sendFileArgs.caption
4199
+ });
4200
+ return;
4201
+ }
4202
+ blocks.push({
4203
+ id: toolCallId,
4204
+ type: "tool",
4205
+ toolCallId,
4206
+ toolName,
4207
+ toolInput: item?.arguments,
4208
+ result: toolResult?.result,
4209
+ isError: toolResult?.isError === true,
4210
+ status: toolResult ? "done" : "running"
4211
+ });
4212
+ });
4213
+ return blocks;
4214
+ }
4215
+ extractThinkingText(item) {
4216
+ if (typeof item?.thinking === "string" && item.thinking.trim()) {
4217
+ return item.thinking.trim();
4218
+ }
4219
+ if (typeof item?.thinkingSignature !== "string" || !item.thinkingSignature) {
4220
+ return "";
4221
+ }
4222
+ try {
4223
+ const parsed = JSON.parse(item.thinkingSignature);
4224
+ if (!Array.isArray(parsed?.summary)) {
4225
+ return "";
4226
+ }
4227
+ return parsed.summary.map(
4228
+ (summaryItem) => typeof summaryItem?.text === "string" ? summaryItem.text.trim() : ""
4229
+ ).filter(Boolean).join("\n\n").trim();
4230
+ } catch {
4231
+ return "";
4232
+ }
4233
+ }
4234
+ extractToolResultValue(message) {
4235
+ if (!message?.content) {
4236
+ return void 0;
4237
+ }
4238
+ if (typeof message.content === "string") {
4239
+ return this.parsePossibleJson(message.content);
4240
+ }
4241
+ if (!Array.isArray(message.content)) {
4242
+ return message.content;
4243
+ }
4244
+ const textContent = message.content.filter((item) => item?.type === "text").map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
4245
+ if (textContent) {
4246
+ return this.parsePossibleJson(textContent);
4247
+ }
4248
+ return message.content;
4249
+ }
4250
+ parsePossibleJson(value) {
4251
+ const trimmed = value.trim();
4252
+ if (!trimmed) {
4253
+ return "";
4254
+ }
4255
+ try {
4256
+ return JSON.parse(trimmed);
4257
+ } catch {
4258
+ return trimmed;
4259
+ }
4260
+ }
4261
+ extractSendFileArguments(rawArguments) {
4262
+ if (!rawArguments || typeof rawArguments !== "object") {
4263
+ return void 0;
3272
4264
  }
4265
+ const maybeArgs = rawArguments;
4266
+ const filePath = typeof maybeArgs.filePath === "string" && maybeArgs.filePath ? maybeArgs.filePath : void 0;
4267
+ const caption = typeof maybeArgs.caption === "string" && maybeArgs.caption ? maybeArgs.caption : void 0;
4268
+ if (!filePath && !caption) {
4269
+ return void 0;
4270
+ }
4271
+ return {
4272
+ filePath,
4273
+ caption
4274
+ };
3273
4275
  }
3274
- isRunning(channelId) {
3275
- return this.channels.get(channelId)?.running ?? false;
4276
+ hasVisibleSendFileToolCall(toolCalls) {
4277
+ return Boolean(
4278
+ toolCalls?.some(
4279
+ (toolCall) => toolCall.name === "send_file" && !toolCall.isError && typeof toolCall.arguments?.filePath === "string" && toolCall.arguments.filePath.length > 0
4280
+ )
4281
+ );
3276
4282
  }
3277
- dispose(channelId) {
3278
- const cs = this.channels.get(channelId);
3279
- if (cs) {
3280
- cs.session.dispose();
3281
- this.channels.delete(channelId);
4283
+ mergeToolCalls(left, right) {
4284
+ const merged = [...left || [], ...right || []];
4285
+ const byId = /* @__PURE__ */ new Map();
4286
+ for (const toolCall of merged) {
4287
+ byId.set(toolCall.id, toolCall);
3282
4288
  }
4289
+ return [...byId.values()];
3283
4290
  }
3284
- /** Reserved: list all sessions */
3285
- listSessions() {
3286
- return [];
4291
+ getFileBaseName(filePath) {
4292
+ const normalized = filePath.replace(/\\/g, "/");
4293
+ const parts = normalized.split("/").filter(Boolean);
4294
+ return parts[parts.length - 1] || filePath;
3287
4295
  }
3288
- /** Reserved: restore a historical session */
3289
- async restoreSession(_sessionId) {
4296
+ detectPlatform(channelId) {
4297
+ if (channelId.startsWith("telegram-")) return "telegram";
4298
+ if (channelId.startsWith("slack-")) return "slack";
4299
+ if (channelId.startsWith("scheduler-")) return "scheduler";
4300
+ return "web";
3290
4301
  }
3291
- getActiveChannelIds() {
3292
- return Array.from(this.channels.keys());
4302
+ isLegacyWebConversation(channelId) {
4303
+ return channelId.startsWith("web-");
3293
4304
  }
3294
4305
  };
3295
4306
 
3296
4307
  // 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
4308
  function getPackConfig(rootDir) {
3303
- const raw = fs10.readFileSync(path10.join(rootDir, "skillpack.json"), "utf-8");
4309
+ const raw = fs15.readFileSync(path15.join(rootDir, "skillpack.json"), "utf-8");
3304
4310
  return JSON.parse(raw);
3305
4311
  }
3306
4312
  function parseCommand(text) {
@@ -3322,15 +4328,30 @@ function getRuntimeConfigSignature(config) {
3322
4328
  slackAppToken: config.adapters?.slack?.appToken || ""
3323
4329
  });
3324
4330
  }
4331
+ function parsePositiveInt(value, fallback) {
4332
+ const parsed = Number(value);
4333
+ if (!Number.isFinite(parsed) || parsed <= 0) {
4334
+ return fallback;
4335
+ }
4336
+ return Math.floor(parsed);
4337
+ }
4338
+ function resolveDownloadFilePath(rootDir, filePath) {
4339
+ const resolvedPath = path15.isAbsolute(filePath) ? path15.resolve(filePath) : path15.resolve(rootDir, filePath);
4340
+ return isWithinDirectory(rootDir, resolvedPath) ? resolvedPath : null;
4341
+ }
3325
4342
  var WebAdapter = class {
3326
4343
  name = "web";
3327
4344
  wss = null;
3328
4345
  agent = null;
3329
4346
  ipcBroadcaster = null;
4347
+ conversationService = null;
4348
+ socketsByChannel = /* @__PURE__ */ new Map();
3330
4349
  async start(ctx) {
3331
4350
  const { agent, server, app, rootDir, lifecycle } = ctx;
3332
4351
  this.agent = agent;
3333
4352
  this.ipcBroadcaster = ctx.ipcBroadcaster ?? null;
4353
+ this.conversationService = new ConversationService(rootDir);
4354
+ const resultsQueryService = ctx.resultsQueryService ?? null;
3334
4355
  const currentConf = configManager.getConfig();
3335
4356
  let apiKey = currentConf.apiKey || "";
3336
4357
  let currentProvider = currentConf.provider || "openai";
@@ -3448,12 +4469,42 @@ var WebAdapter = class {
3448
4469
  app.delete("/api/chat", (_req, res) => {
3449
4470
  res.json({ success: true });
3450
4471
  });
3451
- app.get("/api/sessions", (_req, res) => {
3452
- const sessions = agent.listSessions();
3453
- res.json(sessions);
4472
+ const getWebConversations = () => {
4473
+ const activeChannels = new Set(agent.getActiveChannelIds());
4474
+ return this.conversationService.listConversations(activeChannels, {
4475
+ includeDefaultWeb: true,
4476
+ includeLegacyWeb: false,
4477
+ allowedPlatforms: ["web"]
4478
+ });
4479
+ };
4480
+ app.get("/api/conversations", (_req, res) => {
4481
+ res.json(getWebConversations());
4482
+ });
4483
+ app.post("/api/conversations", (_req, res) => {
4484
+ res.json({ channelId: DEFAULT_WEB_CHANNEL_ID });
4485
+ });
4486
+ app.get("/api/conversations/:channelId/messages", (req, res) => {
4487
+ if (req.params.channelId !== DEFAULT_WEB_CHANNEL_ID) {
4488
+ res.status(404).json({ error: "Conversation not found" });
4489
+ return;
4490
+ }
4491
+ res.json(
4492
+ this.conversationService.getMessages(
4493
+ req.params.channelId,
4494
+ parsePositiveInt(req.query.limit, 100)
4495
+ )
4496
+ );
3454
4497
  });
3455
- app.get("/api/sessions/:id", (_req, res) => {
3456
- res.status(501).json({ error: "Not implemented yet" });
4498
+ app.get("/api/results/artifacts", (req, res) => {
4499
+ if (!resultsQueryService) {
4500
+ res.status(503).json({ error: "Results query service is not available" });
4501
+ return;
4502
+ }
4503
+ res.json(resultsQueryService.listRecentArtifacts({
4504
+ channelId: typeof req.query.channelId === "string" ? req.query.channelId : void 0,
4505
+ limit: parsePositiveInt(req.query.limit, 100),
4506
+ offset: parsePositiveInt(req.query.offset, 0)
4507
+ }));
3457
4508
  });
3458
4509
  app.get("/api/files", (req, res) => {
3459
4510
  const filePath = req.query.path;
@@ -3461,23 +4512,22 @@ var WebAdapter = class {
3461
4512
  res.status(400).json({ error: "Missing 'path' query parameter" });
3462
4513
  return;
3463
4514
  }
3464
- const resolvedPath = path10.resolve(filePath);
3465
- const dataDir = path10.resolve(rootDir, "data");
3466
- if (!resolvedPath.startsWith(dataDir)) {
4515
+ const resolvedPath = resolveDownloadFilePath(rootDir, filePath);
4516
+ if (!resolvedPath) {
3467
4517
  res.status(403).json({ error: "Access denied" });
3468
4518
  return;
3469
4519
  }
3470
- if (!fs10.existsSync(resolvedPath)) {
4520
+ if (!fs15.existsSync(resolvedPath)) {
3471
4521
  res.status(404).json({ error: "File not found" });
3472
4522
  return;
3473
4523
  }
3474
- const filename = path10.basename(resolvedPath);
4524
+ const filename = path15.basename(resolvedPath);
3475
4525
  res.setHeader("Content-Type", "application/octet-stream");
3476
4526
  res.setHeader(
3477
4527
  "Content-Disposition",
3478
4528
  `attachment; filename="${filename}"`
3479
4529
  );
3480
- fs10.createReadStream(resolvedPath).pipe(res);
4530
+ fs15.createReadStream(resolvedPath).pipe(res);
3481
4531
  });
3482
4532
  const getScheduler = () => {
3483
4533
  const schedulerAdapter = ctx.adapterMap?.get("scheduler");
@@ -3571,7 +4621,8 @@ var WebAdapter = class {
3571
4621
  ws.close();
3572
4622
  return;
3573
4623
  }
3574
- const channelId = `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4624
+ const requestedChannelId = url.searchParams.get("channelId");
4625
+ const channelId = requestedChannelId && requestedChannelId === DEFAULT_WEB_CHANNEL_ID ? requestedChannelId : DEFAULT_WEB_CHANNEL_ID;
3575
4626
  this.handleWsConnection(ws, channelId, agent);
3576
4627
  });
3577
4628
  console.log("[WebAdapter] Started");
@@ -3584,12 +4635,29 @@ var WebAdapter = class {
3584
4635
  this.wss.close();
3585
4636
  this.wss = null;
3586
4637
  }
4638
+ this.socketsByChannel.clear();
3587
4639
  console.log("[WebAdapter] Stopped");
3588
4640
  }
4641
+ async sendMessage(channelId, text) {
4642
+ const sockets = this.socketsByChannel.get(channelId);
4643
+ const activeSockets = [...sockets || []].filter(
4644
+ (socket) => socket.readyState === socket.OPEN
4645
+ );
4646
+ if (activeSockets.length === 0) {
4647
+ throw new Error(`[Web] No active WebSocket clients for channelId: ${channelId}`);
4648
+ }
4649
+ for (const socket of activeSockets) {
4650
+ sendWsEvent(socket, { type: "message_start", role: "assistant" });
4651
+ sendWsEvent(socket, { type: "text_delta", delta: text });
4652
+ sendWsEvent(socket, { type: "message_end", role: "assistant" });
4653
+ socket.send(JSON.stringify({ done: true }));
4654
+ }
4655
+ }
3589
4656
  // -------------------------------------------------------------------------
3590
4657
  // WebSocket message handler
3591
4658
  // -------------------------------------------------------------------------
3592
4659
  handleWsConnection(ws, channelId, agent) {
4660
+ this.addSocket(channelId, ws);
3593
4661
  ws.on("message", async (data) => {
3594
4662
  try {
3595
4663
  const payload = JSON.parse(data.toString());
@@ -3619,154 +4687,27 @@ var WebAdapter = class {
3619
4687
  }
3620
4688
  });
3621
4689
  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
- }
4690
+ this.removeSocket(channelId, ws);
4691
+ if (channelId !== DEFAULT_WEB_CHANNEL_ID) {
4692
+ agent.dispose(channelId);
3675
4693
  }
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
4694
  });
3690
4695
  }
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);
4696
+ addSocket(channelId, ws) {
4697
+ const sockets = this.socketsByChannel.get(channelId) ?? /* @__PURE__ */ new Set();
4698
+ sockets.add(ws);
4699
+ this.socketsByChannel.set(channelId, sockets);
3723
4700
  }
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;
4701
+ removeSocket(channelId, ws) {
4702
+ const sockets = this.socketsByChannel.get(channelId);
4703
+ if (!sockets) {
4704
+ return;
3731
4705
  }
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 [];
4706
+ sockets.delete(ws);
4707
+ if (sockets.size === 0) {
4708
+ this.socketsByChannel.delete(channelId);
3744
4709
  }
3745
4710
  }
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
4711
  };
3771
4712
 
3772
4713
  // src/runtime/adapters/ipc.ts
@@ -3777,6 +4718,7 @@ var IpcAdapter = class {
3777
4718
  rootDir = "";
3778
4719
  adapterMap = null;
3779
4720
  conversationService = null;
4721
+ resultsQueryService = null;
3780
4722
  createdChannels = /* @__PURE__ */ new Set();
3781
4723
  messageListener;
3782
4724
  started = false;
@@ -3788,6 +4730,7 @@ var IpcAdapter = class {
3788
4730
  this.rootDir = ctx.rootDir;
3789
4731
  this.adapterMap = ctx.adapterMap ?? null;
3790
4732
  this.conversationService = new ConversationService(ctx.rootDir);
4733
+ this.resultsQueryService = ctx.resultsQueryService ?? null;
3791
4734
  this.messageListener = (message) => {
3792
4735
  if (!this.isIpcRequest(message)) return;
3793
4736
  void this.handleRequest(message);
@@ -3846,12 +4789,15 @@ var IpcAdapter = class {
3846
4789
  for (const channelId of this.createdChannels) {
3847
4790
  activeChannels.add(channelId);
3848
4791
  }
3849
- const conversations = this.conversationService.listConversations(activeChannels);
4792
+ const conversations = this.conversationService.listConversations(activeChannels, {
4793
+ includeDefaultWeb: true,
4794
+ includeLegacyWeb: false
4795
+ });
3850
4796
  this.reply(request.id, conversations);
3851
4797
  return;
3852
4798
  }
3853
4799
  case "create_conversation": {
3854
- const channelId = `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4800
+ const channelId = DEFAULT_WEB_CHANNEL_ID;
3855
4801
  this.createdChannels.add(channelId);
3856
4802
  this.reply(request.id, { channelId });
3857
4803
  return;
@@ -3868,6 +4814,18 @@ var IpcAdapter = class {
3868
4814
  this.reply(request.id, messages);
3869
4815
  return;
3870
4816
  }
4817
+ case "get_recent_artifacts": {
4818
+ if (!this.resultsQueryService) {
4819
+ this.replyError(request.id, "Results query service is not available");
4820
+ return;
4821
+ }
4822
+ this.reply(request.id, this.resultsQueryService.listRecentArtifacts({
4823
+ channelId: request.channelId,
4824
+ limit: request.limit,
4825
+ offset: request.offset
4826
+ }));
4827
+ return;
4828
+ }
3871
4829
  case "send_message": {
3872
4830
  if (!request.channelId || typeof request.channelId !== "string") {
3873
4831
  this.replyError(request.id, "channelId is required");
@@ -3950,6 +4908,48 @@ var IpcAdapter = class {
3950
4908
  this.reply(request.id, result);
3951
4909
  return;
3952
4910
  }
4911
+ case "update_scheduled_job": {
4912
+ const scheduler = this.getSchedulerAdapter();
4913
+ if (!scheduler) {
4914
+ this.replyError(request.id, "Scheduler adapter is not available");
4915
+ return;
4916
+ }
4917
+ const result = scheduler.updateJob(request.name, request.updates);
4918
+ if (!result.success) {
4919
+ this.replyError(request.id, result.message);
4920
+ return;
4921
+ }
4922
+ this.reply(request.id, result);
4923
+ return;
4924
+ }
4925
+ case "set_scheduled_job_enabled": {
4926
+ const scheduler = this.getSchedulerAdapter();
4927
+ if (!scheduler) {
4928
+ this.replyError(request.id, "Scheduler adapter is not available");
4929
+ return;
4930
+ }
4931
+ const result = scheduler.setEnabled(request.name, request.enabled);
4932
+ if (!result.success) {
4933
+ this.replyError(request.id, result.message);
4934
+ return;
4935
+ }
4936
+ this.reply(request.id, result);
4937
+ return;
4938
+ }
4939
+ case "trigger_scheduled_job": {
4940
+ const scheduler = this.getSchedulerAdapter();
4941
+ if (!scheduler) {
4942
+ this.replyError(request.id, "Scheduler adapter is not available");
4943
+ return;
4944
+ }
4945
+ const result = await scheduler.triggerJob(request.name);
4946
+ if (!result.success) {
4947
+ this.replyError(request.id, result.message);
4948
+ return;
4949
+ }
4950
+ this.reply(request.id, result);
4951
+ return;
4952
+ }
3953
4953
  case "remove_scheduled_job": {
3954
4954
  const scheduler = this.getSchedulerAdapter();
3955
4955
  if (!scheduler) {
@@ -3996,9 +4996,6 @@ var IpcAdapter = class {
3996
4996
  }
3997
4997
  };
3998
4998
 
3999
- // src/runtime/server.ts
4000
- init_config();
4001
-
4002
4999
  // src/runtime/lifecycle.ts
4003
5000
  var SHUTDOWN_EXIT_CODE = 64;
4004
5001
  var RESTART_EXIT_CODE = 75;
@@ -4072,28 +5069,28 @@ var Lifecycle = class {
4072
5069
 
4073
5070
  // src/runtime/registry.ts
4074
5071
  import crypto from "crypto";
4075
- import fs12 from "fs";
5072
+ import fs16 from "fs";
4076
5073
  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");
5074
+ import path16 from "path";
5075
+ var SKILLPACK_HOME = path16.join(os.homedir(), ".skillpack");
5076
+ var LEGACY_REGISTRY_FILE = path16.join(SKILLPACK_HOME, "registry.json");
5077
+ var REGISTRY_DIR = path16.join(SKILLPACK_HOME, "registry.d");
4081
5078
  var migrationChecked = false;
4082
5079
  function ensureHomeDir() {
4083
- if (!fs12.existsSync(SKILLPACK_HOME)) {
4084
- fs12.mkdirSync(SKILLPACK_HOME, { recursive: true });
5080
+ if (!fs16.existsSync(SKILLPACK_HOME)) {
5081
+ fs16.mkdirSync(SKILLPACK_HOME, { recursive: true });
4085
5082
  }
4086
5083
  }
4087
5084
  function ensureRegistryDir() {
4088
5085
  ensureHomeDir();
4089
- if (!fs12.existsSync(REGISTRY_DIR)) {
4090
- fs12.mkdirSync(REGISTRY_DIR, { recursive: true });
5086
+ if (!fs16.existsSync(REGISTRY_DIR)) {
5087
+ fs16.mkdirSync(REGISTRY_DIR, { recursive: true });
4091
5088
  }
4092
5089
  }
4093
5090
  function canonicalizeDir(dir) {
4094
- const resolved = path12.resolve(dir);
5091
+ const resolved = path16.resolve(dir);
4095
5092
  try {
4096
- return fs12.realpathSync(resolved);
5093
+ return fs16.realpathSync(resolved);
4097
5094
  } catch {
4098
5095
  return resolved;
4099
5096
  }
@@ -4102,7 +5099,7 @@ function hashDir(dir) {
4102
5099
  return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
4103
5100
  }
4104
5101
  function getEntryPathForCanonicalDir(dir) {
4105
- return path12.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
5102
+ return path16.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
4106
5103
  }
4107
5104
  function getEntryPath(dir) {
4108
5105
  ensureRegistryReady();
@@ -4110,11 +5107,11 @@ function getEntryPath(dir) {
4110
5107
  }
4111
5108
  function listEntryFiles() {
4112
5109
  ensureRegistryReady();
4113
- return fs12.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path12.join(REGISTRY_DIR, file));
5110
+ return fs16.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path16.join(REGISTRY_DIR, file));
4114
5111
  }
4115
5112
  function readEntryFile(filePath) {
4116
5113
  try {
4117
- const raw = fs12.readFileSync(filePath, "utf-8");
5114
+ const raw = fs16.readFileSync(filePath, "utf-8");
4118
5115
  const data = JSON.parse(raw);
4119
5116
  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
5117
  return null;
@@ -4147,8 +5144,8 @@ function writeEntryFile(entry) {
4147
5144
  };
4148
5145
  const entryPath = getEntryPathForCanonicalDir(normalized.dir);
4149
5146
  const tmpPath = createTmpPath(entryPath);
4150
- fs12.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
4151
- fs12.renameSync(tmpPath, entryPath);
5147
+ fs16.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
5148
+ fs16.renameSync(tmpPath, entryPath);
4152
5149
  }
4153
5150
  function migrateLegacyRegistryIfNeeded() {
4154
5151
  if (migrationChecked) {
@@ -4156,14 +5153,14 @@ function migrateLegacyRegistryIfNeeded() {
4156
5153
  }
4157
5154
  migrationChecked = true;
4158
5155
  ensureRegistryDir();
4159
- if (!fs12.existsSync(LEGACY_REGISTRY_FILE)) {
5156
+ if (!fs16.existsSync(LEGACY_REGISTRY_FILE)) {
4160
5157
  return;
4161
5158
  }
4162
5159
  if (listEntryFiles().length > 0) {
4163
5160
  return;
4164
5161
  }
4165
5162
  try {
4166
- const raw = fs12.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
5163
+ const raw = fs16.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
4167
5164
  const data = JSON.parse(raw);
4168
5165
  const packs = Array.isArray(data?.packs) ? data.packs : [];
4169
5166
  for (const pack of packs) {
@@ -4176,7 +5173,7 @@ function migrateLegacyRegistryIfNeeded() {
4176
5173
  } catch {
4177
5174
  }
4178
5175
  }
4179
- fs12.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
5176
+ fs16.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
4180
5177
  } catch (err) {
4181
5178
  console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
4182
5179
  }
@@ -4229,13 +5226,13 @@ function deregister(dir, pid) {
4229
5226
  }
4230
5227
 
4231
5228
  // src/runtime/server.ts
4232
- var __dirname = path14.dirname(fileURLToPath2(import.meta.url));
5229
+ var __dirname = path18.dirname(fileURLToPath2(import.meta.url));
4233
5230
  async function startServer(options) {
4234
5231
  const {
4235
5232
  rootDir,
4236
5233
  host = process.env.HOST || "127.0.0.1",
4237
5234
  port = Number(process.env.PORT) || 26313,
4238
- daemonRun = false
5235
+ runtimeMode = process.env.SKILLPACK_RUNTIME_MODE === "embedded" ? "embedded" : "standalone"
4239
5236
  } = options;
4240
5237
  const dataConfig = configManager.load(rootDir);
4241
5238
  const apiKey = dataConfig.apiKey || "";
@@ -4245,8 +5242,8 @@ async function startServer(options) {
4245
5242
  const baseUrl = dataConfig.baseUrl?.trim() || void 0;
4246
5243
  const modelId = dataConfig.modelId?.trim() || (SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId);
4247
5244
  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");
5245
+ const packageRoot = path18.resolve(__dirname, "..");
5246
+ const webDir = fs19.existsSync(path18.join(rootDir, "web")) ? path18.join(rootDir, "web") : path18.join(packageRoot, "web");
4250
5247
  const app = express();
4251
5248
  app.use(express.json());
4252
5249
  app.use(express.static(webDir));
@@ -4264,6 +5261,13 @@ async function startServer(options) {
4264
5261
  });
4265
5262
  });
4266
5263
  const lifecycle = new Lifecycle(server);
5264
+ const resultStore = new ResultStore(rootDir);
5265
+ const artifactSnapshotService = new ArtifactSnapshotService(rootDir);
5266
+ const artifactPersistenceService = new ArtifactPersistenceService(
5267
+ artifactSnapshotService,
5268
+ resultStore
5269
+ );
5270
+ const resultsQueryService = new ResultsQueryService(resultStore);
4267
5271
  const agent = new PackAgent({
4268
5272
  apiKey,
4269
5273
  rootDir,
@@ -4271,12 +5275,14 @@ async function startServer(options) {
4271
5275
  modelId,
4272
5276
  baseUrl,
4273
5277
  apiProtocol,
4274
- lifecycleHandler: lifecycle
5278
+ lifecycleHandler: lifecycle,
5279
+ artifactPersistenceService
4275
5280
  });
4276
5281
  const adapters = [];
4277
5282
  const adapterMap = /* @__PURE__ */ new Map();
4278
5283
  const hasIpcChannel = typeof process.send === "function";
4279
5284
  const ipcAdapter = new IpcAdapter();
5285
+ const webEnabled = runtimeMode === "standalone";
4280
5286
  if (hasIpcChannel) {
4281
5287
  await ipcAdapter.start({
4282
5288
  agent,
@@ -4284,24 +5290,28 @@ async function startServer(options) {
4284
5290
  app,
4285
5291
  rootDir,
4286
5292
  lifecycle,
4287
- adapterMap
5293
+ adapterMap,
5294
+ resultsQueryService
4288
5295
  });
4289
5296
  adapters.push(ipcAdapter);
4290
5297
  adapterMap.set(ipcAdapter.name, ipcAdapter);
4291
5298
  }
4292
5299
  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);
5300
+ if (webEnabled) {
5301
+ const webAdapter = new WebAdapter();
5302
+ await webAdapter.start({
5303
+ agent,
5304
+ server,
5305
+ app,
5306
+ rootDir,
5307
+ lifecycle,
5308
+ adapterMap,
5309
+ ipcBroadcaster,
5310
+ resultsQueryService
5311
+ });
5312
+ adapters.push(webAdapter);
5313
+ adapterMap.set(webAdapter.name, webAdapter);
5314
+ }
4305
5315
  if (dataConfig.adapters?.telegram?.token) {
4306
5316
  try {
4307
5317
  const { TelegramAdapter: TelegramAdapter2 } = await Promise.resolve().then(() => (init_telegram(), telegram_exports));
@@ -4315,7 +5325,8 @@ async function startServer(options) {
4315
5325
  rootDir,
4316
5326
  lifecycle,
4317
5327
  adapterMap,
4318
- ipcBroadcaster
5328
+ ipcBroadcaster,
5329
+ resultsQueryService
4319
5330
  });
4320
5331
  adapters.push(telegramAdapter);
4321
5332
  adapterMap.set(telegramAdapter.name, telegramAdapter);
@@ -4343,7 +5354,8 @@ async function startServer(options) {
4343
5354
  rootDir,
4344
5355
  lifecycle,
4345
5356
  adapterMap,
4346
- ipcBroadcaster
5357
+ ipcBroadcaster,
5358
+ resultsQueryService
4347
5359
  });
4348
5360
  adapters.push(slackAdapter);
4349
5361
  adapterMap.set(slackAdapter.name, slackAdapter);
@@ -4363,7 +5375,6 @@ async function startServer(options) {
4363
5375
  }
4364
5376
  await adapter.sendMessage(channelId, text);
4365
5377
  };
4366
- const scheduledJobs = dataConfig.scheduledJobs || [];
4367
5378
  let schedulerAdapter = null;
4368
5379
  try {
4369
5380
  const { SchedulerAdapter: SchedulerAdapter2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
@@ -4375,13 +5386,11 @@ async function startServer(options) {
4375
5386
  rootDir,
4376
5387
  lifecycle,
4377
5388
  notify: notifyFn,
4378
- adapterMap
5389
+ adapterMap,
5390
+ resultsQueryService
4379
5391
  });
4380
5392
  adapters.push(schedulerAdapter);
4381
5393
  adapterMap.set(schedulerAdapter.name, schedulerAdapter);
4382
- if (scheduledJobs.length > 0) {
4383
- console.log(`[Server] Scheduler started with ${scheduledJobs.length} job(s)`);
4384
- }
4385
5394
  } catch (err) {
4386
5395
  console.error("[Scheduler] Failed to start:", err);
4387
5396
  }
@@ -4389,34 +5398,35 @@ async function startServer(options) {
4389
5398
  agent.setScheduler(schedulerAdapter);
4390
5399
  }
4391
5400
  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(`
5401
+ const announceReady = (actualPort) => {
5402
+ if (webEnabled) {
5403
+ const url = `http://${host}:${actualPort}`;
5404
+ console.log(`
4397
5405
  Skills Pack Server`);
4398
- console.log(` Running at ${url}
5406
+ console.log(` Running at ${url}
4399
5407
  `);
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) {
5408
+ try {
5409
+ register({
5410
+ dir: canonicalRootDir,
5411
+ name: packConfig.name,
5412
+ version: packConfig.version,
5413
+ port: actualPort
5414
+ });
5415
+ } catch (err) {
5416
+ console.warn(" [Registry] Could not register pack:", err);
5417
+ }
4414
5418
  const cmd = process.platform === "darwin" ? `open ${url}` : process.platform === "win32" ? `start ${url}` : `xdg-open ${url}`;
4415
5419
  exec(cmd, (err) => {
4416
5420
  if (err) console.warn(` Could not open browser: ${err.message}`);
4417
5421
  });
5422
+ } else {
5423
+ console.log("\n Skills Pack Server");
5424
+ console.log(" Running in embedded mode (IPC only)\n");
4418
5425
  }
4419
- });
5426
+ if (hasIpcChannel) {
5427
+ ipcAdapter.notifyReady(actualPort);
5428
+ }
5429
+ };
4420
5430
  process.on("SIGINT", () => {
4421
5431
  deregister(canonicalRootDir, process.pid);
4422
5432
  void lifecycle.requestShutdown("signal");
@@ -4425,22 +5435,30 @@ async function startServer(options) {
4425
5435
  deregister(canonicalRootDir, process.pid);
4426
5436
  void lifecycle.requestShutdown("signal");
4427
5437
  });
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
- });
5438
+ if (webEnabled) {
5439
+ const actualPort = await new Promise((resolve, reject) => {
5440
+ function tryListen(listenPort) {
5441
+ server.listen(listenPort, host);
5442
+ server.once("error", (err) => {
5443
+ if (err.code === "EADDRINUSE") {
5444
+ console.log(` Port ${listenPort} is in use, trying ${listenPort + 1}...`);
5445
+ server.close();
5446
+ tryListen(listenPort + 1);
5447
+ } else {
5448
+ reject(err);
5449
+ }
5450
+ });
5451
+ server.once("listening", () => {
5452
+ const address = server.address();
5453
+ resolve(typeof address === "string" ? listenPort : address?.port ?? listenPort);
5454
+ });
5455
+ }
5456
+ tryListen(port);
5457
+ });
5458
+ announceReady(actualPort);
5459
+ } else {
5460
+ announceReady(0);
5461
+ }
4444
5462
  await new Promise(() => {
4445
5463
  });
4446
5464
  }
@@ -4457,23 +5475,23 @@ function findMissingSkills(workDir, config) {
4457
5475
  });
4458
5476
  }
4459
5477
  function copyStartTemplates2(workDir) {
4460
- const templateDir = path15.resolve(
5478
+ const templateDir = path19.resolve(
4461
5479
  new URL("../templates", import.meta.url).pathname
4462
5480
  );
4463
5481
  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);
5482
+ const src = path19.join(templateDir, file);
5483
+ const dest = path19.join(workDir, file);
5484
+ if (fs20.existsSync(src)) {
5485
+ fs20.copyFileSync(src, dest);
4468
5486
  if (file === "start.sh") {
4469
- fs16.chmodSync(dest, 493);
5487
+ fs20.chmodSync(dest, 493);
4470
5488
  }
4471
5489
  }
4472
5490
  }
4473
5491
  }
4474
5492
  async function runCommand(directory) {
4475
- const workDir = directory ? path15.resolve(directory) : process.cwd();
4476
- fs16.mkdirSync(workDir, { recursive: true });
5493
+ const workDir = directory ? path19.resolve(directory) : process.cwd();
5494
+ fs20.mkdirSync(workDir, { recursive: true });
4477
5495
  if (!configExists(workDir)) {
4478
5496
  console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
4479
5497
  const { name, description } = await inquirer2.prompt([
@@ -4512,20 +5530,19 @@ async function runCommand(directory) {
4512
5530
  syncSkillDescriptions(workDir, config);
4513
5531
  saveConfig(workDir, config);
4514
5532
  await startServer({
4515
- rootDir: workDir,
4516
- daemonRun: process.env.DAEMON_RUN === "1"
5533
+ rootDir: workDir
4517
5534
  });
4518
5535
  }
4519
5536
 
4520
5537
  // src/cli.ts
4521
- import fs17 from "fs";
4522
- import path16 from "path";
5538
+ import fs21 from "fs";
5539
+ import path20 from "path";
4523
5540
  import { fileURLToPath as fileURLToPath3 } from "url";
4524
5541
  var packageJson = JSON.parse(
4525
- fs17.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
5542
+ fs21.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
4526
5543
  );
4527
5544
  var program = new Command();
4528
- var cliFilePath = path16.resolve(fileURLToPath3(import.meta.url));
5545
+ var cliFilePath = path20.resolve(fileURLToPath3(import.meta.url));
4529
5546
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
4530
5547
  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
5548
  await createCommand(directory, options);
@@ -4535,7 +5552,7 @@ program.command("run [directory]").description("Start the SkillPack runtime serv
4535
5552
  if (options?.host) process.env.HOST = options.host;
4536
5553
  await runCommand(directory);
4537
5554
  });
4538
- program.command("zip").description("Package the current pack as a zip file (skillpack.json + skills/ + start scripts)").action(async () => {
5555
+ program.command("zip").description("Package the current pack as a zip file (skillpack.json + optional job.json + skills/ + start scripts)").action(async () => {
4539
5556
  try {
4540
5557
  await zipCommand(process.cwd());
4541
5558
  } catch (err) {
@@ -4546,7 +5563,7 @@ program.command("zip").description("Package the current pack as a zip file (skil
4546
5563
  function normalizeUserArgs(argv) {
4547
5564
  if (argv.length === 0) return argv;
4548
5565
  const firstArg = argv[0];
4549
- if (firstArg && path16.resolve(firstArg) === cliFilePath) {
5566
+ if (firstArg && path20.resolve(firstArg) === cliFilePath) {
4550
5567
  return argv.slice(1);
4551
5568
  }
4552
5569
  return argv;