@serviceme/devtools-core 0.0.6 → 0.1.3

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/index.js CHANGED
@@ -30,23 +30,39 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DaemonLogger: () => DaemonLogger,
33
34
  EnvironmentInspector: () => EnvironmentInspector,
35
+ GithubCopilotCliExecutor: () => GithubCopilotCliExecutor,
36
+ HttpRequestExecutor: () => HttpRequestExecutor,
34
37
  ImageTools: () => ImageTools,
35
- OpenCodeManager: () => OpenCodeManager,
38
+ PidManager: () => PidManager,
36
39
  ProjectTools: () => ProjectTools,
40
+ SchedulerDaemon: () => SchedulerDaemon,
41
+ ShellExecutor: () => ShellExecutor,
42
+ TaskConfigManager: () => TaskConfigManager,
43
+ TaskExecutionEngine: () => TaskExecutionEngine,
44
+ TaskLogManager: () => TaskLogManager,
45
+ copilotDoctor: () => copilotDoctor,
46
+ copilotPrompt: () => copilotPrompt,
37
47
  createConsoleLogger: () => createConsoleLogger,
48
+ createCopilotAuthRequiredError: () => createCopilotAuthRequiredError,
49
+ createCopilotNotInstalledError: () => createCopilotNotInstalledError,
38
50
  createImageTools: () => createImageTools,
39
51
  createJsonTools: () => createJsonTools,
40
52
  createProjectTools: () => createProjectTools,
53
+ getExecutor: () => getExecutor,
54
+ isCopilotAuthenticated: () => isCopilotAuthenticated,
55
+ isStreamingTaskExecutor: () => isStreamingTaskExecutor,
56
+ matchesCron: () => matchesCron,
41
57
  moveFiles: () => moveFiles,
42
58
  noopLogger: () => noopLogger,
59
+ parseIntervalMs: () => parseIntervalMs,
43
60
  unzipFile: () => unzipFile
44
61
  });
45
62
  module.exports = __toCommonJS(index_exports);
46
63
 
47
- // src/env/environmentInspector.ts
64
+ // src/copilot/doctor.ts
48
65
  var import_devtools_protocol = require("@serviceme/devtools-protocol");
49
- var import_devtools_protocol2 = require("@serviceme/devtools-protocol");
50
66
 
51
67
  // src/process/runCommand.ts
52
68
  var import_node_child_process = require("child_process");
@@ -156,19 +172,138 @@ async function commandExists(command) {
156
172
  }
157
173
  }
158
174
 
175
+ // src/copilot/doctor.ts
176
+ var COPILOT_COMMAND = "copilot";
177
+ var GH_COMMAND = "gh";
178
+ async function isCopilotAuthenticated() {
179
+ try {
180
+ await runCommand(GH_COMMAND, {
181
+ args: ["auth", "status"],
182
+ timeoutMs: 1e4
183
+ });
184
+ return true;
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+ async function copilotDoctor() {
190
+ const exists = await commandExists(COPILOT_COMMAND);
191
+ if (!exists) {
192
+ return { installed: false, version: null, authenticated: false };
193
+ }
194
+ let version = null;
195
+ try {
196
+ const versionResult = await runCommand(COPILOT_COMMAND, {
197
+ args: ["--version"],
198
+ timeoutMs: 1e4
199
+ });
200
+ version = versionResult.stdout.trim();
201
+ } catch {
202
+ return { installed: true, version: null, authenticated: false };
203
+ }
204
+ const authenticated = await isCopilotAuthenticated();
205
+ return { installed: true, version, authenticated };
206
+ }
207
+ function createCopilotNotInstalledError() {
208
+ return (0, import_devtools_protocol.createServicemeError)(
209
+ import_devtools_protocol.COPILOT_ERROR_CODES.NOT_INSTALLED,
210
+ "GitHub Copilot CLI is not installed. Visit https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line to install."
211
+ );
212
+ }
213
+ function createCopilotAuthRequiredError() {
214
+ return (0, import_devtools_protocol.createServicemeError)(
215
+ import_devtools_protocol.COPILOT_ERROR_CODES.AUTH_REQUIRED,
216
+ "GitHub Copilot CLI requires authentication. Run `copilot auth login` to sign in."
217
+ );
218
+ }
219
+
220
+ // src/copilot/prompt.ts
221
+ var import_devtools_protocol2 = require("@serviceme/devtools-protocol");
222
+ var COPILOT_COMMAND2 = "copilot";
223
+ var DEFAULT_TIMEOUT_MS = 12e4;
224
+ function stripAnsi(text) {
225
+ return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
226
+ }
227
+ async function copilotPrompt(options) {
228
+ const args = ["-p", options.prompt];
229
+ if (options.workspace) {
230
+ args.push("--add-dir", options.workspace);
231
+ }
232
+ if (options.autopilot) {
233
+ args.push("--allow-all-tools");
234
+ }
235
+ if (options.allowTools?.length) {
236
+ for (const tool of options.allowTools) {
237
+ args.push("--allow-tool", tool);
238
+ }
239
+ }
240
+ if (options.model) {
241
+ args.push("--model", options.model);
242
+ }
243
+ if (options.agent) {
244
+ args.push("--agent", options.agent);
245
+ }
246
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
247
+ try {
248
+ const result = await runCommand(COPILOT_COMMAND2, {
249
+ args,
250
+ cwd: options.workspace,
251
+ timeoutMs: timeout
252
+ });
253
+ const output = stripAnsi(result.stdout);
254
+ if (output.includes("I'm sorry, but I cannot assist") && result.stderr?.includes("Stream completed without a response")) {
255
+ throw (0, import_devtools_protocol2.createServicemeError)(
256
+ import_devtools_protocol2.COPILOT_ERROR_CODES.AUTH_REQUIRED,
257
+ "Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
258
+ { exitCode: 0, output, stderr: result.stderr }
259
+ );
260
+ }
261
+ return { output, exitCode: 0 };
262
+ } catch (error) {
263
+ const err = error;
264
+ if (err.code === "ETIMEDOUT") {
265
+ throw (0, import_devtools_protocol2.createServicemeError)(
266
+ import_devtools_protocol2.COPILOT_ERROR_CODES.TIMEOUT,
267
+ `Copilot prompt timed out after ${String(timeout)}ms.`
268
+ );
269
+ }
270
+ const exitCode = typeof err.code === "number" ? err.code : 1;
271
+ const output = stripAnsi(err.stdout ?? "");
272
+ const stderr = err.stderr ?? err.message ?? "";
273
+ if (output.includes("I'm sorry, but I cannot assist") || stderr.includes("Stream completed without a response")) {
274
+ throw (0, import_devtools_protocol2.createServicemeError)(
275
+ import_devtools_protocol2.COPILOT_ERROR_CODES.AUTH_REQUIRED,
276
+ "Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
277
+ { exitCode, output, stderr }
278
+ );
279
+ }
280
+ if (exitCode !== 0 && output) {
281
+ return { output, exitCode };
282
+ }
283
+ throw (0, import_devtools_protocol2.createServicemeError)(
284
+ import_devtools_protocol2.COPILOT_ERROR_CODES.EXECUTION_FAILED,
285
+ stderr || `Copilot exited with code ${String(exitCode)}.`,
286
+ { exitCode, stderr }
287
+ );
288
+ }
289
+ }
290
+
159
291
  // src/env/environmentInspector.ts
292
+ var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
160
293
  var TOOL_CHECK_TIMEOUT_MS = 5e3;
161
294
  var ERROR_CODE_NOT_FOUND = 127;
162
295
  var EnvironmentInspector = class {
163
296
  async checkEnvironment() {
164
297
  const results = await Promise.all(
165
- import_devtools_protocol.KNOWN_ENVIRONMENT_TOOLS.map(async (tool) => [tool, await this.checkTool(tool)])
298
+ import_devtools_protocol3.KNOWN_ENVIRONMENT_TOOLS.map(
299
+ async (tool) => [tool, await this.checkTool(tool)]
300
+ )
166
301
  );
167
302
  return Object.fromEntries(results);
168
303
  }
169
304
  async checkTool(toolName) {
170
305
  if (!/^[a-zA-Z0-9-]+$/.test(toolName)) {
171
- throw (0, import_devtools_protocol2.createServicemeError)("invalid_params", "Invalid tool name.");
306
+ throw (0, import_devtools_protocol3.createServicemeError)("invalid_params", "Invalid tool name.");
172
307
  }
173
308
  try {
174
309
  if (toolName === "nvm") {
@@ -325,8 +460,16 @@ var EnvironmentInspector = class {
325
460
  // src/image/imageTools.ts
326
461
  var fs = __toESM(require("fs/promises"));
327
462
  var path = __toESM(require("path"));
328
- var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
329
- var SUPPORTED_FORMATS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff"];
463
+ var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
464
+ var SUPPORTED_FORMATS = [
465
+ ".jpg",
466
+ ".jpeg",
467
+ ".png",
468
+ ".webp",
469
+ ".gif",
470
+ ".bmp",
471
+ ".tiff"
472
+ ];
330
473
  var DEFAULT_MINIMUM_COMPRESSION_RATIO = 5;
331
474
  var ImageTools = class {
332
475
  getSupportedFormats() {
@@ -361,7 +504,10 @@ var ImageTools = class {
361
504
  async compress(imagePath, options) {
362
505
  const validation = await this.validate(imagePath, options.sharpModulePath);
363
506
  if (!validation.valid) {
364
- throw (0, import_devtools_protocol3.createServicemeError)("invalid_params", `Invalid image file: ${imagePath}`);
507
+ throw (0, import_devtools_protocol4.createServicemeError)(
508
+ "invalid_params",
509
+ `Invalid image file: ${imagePath}`
510
+ );
365
511
  }
366
512
  const originalStats = await fs.stat(imagePath);
367
513
  const originalSize = originalStats.size;
@@ -432,7 +578,7 @@ var ImageTools = class {
432
578
  try {
433
579
  return sharpModulePath ? require(sharpModulePath) : require("sharp");
434
580
  } catch (error) {
435
- throw (0, import_devtools_protocol3.createServicemeError)(
581
+ throw (0, import_devtools_protocol4.createServicemeError)(
436
582
  "internal_error",
437
583
  `sharp is required for image operations: ${error instanceof Error ? error.message : String(error)}`
438
584
  );
@@ -444,14 +590,13 @@ function createImageTools() {
444
590
  }
445
591
 
446
592
  // src/json/jsonTools.ts
593
+ var import_devtools_protocol5 = require("@serviceme/devtools-protocol");
447
594
  var import_comment_json = require("comment-json");
448
595
  var import_json5 = __toESM(require("json5"));
449
- var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
450
- var import_devtools_protocol5 = require("@serviceme/devtools-protocol");
451
596
  function createJsonTools() {
452
597
  return {
453
598
  sort(text, options) {
454
- const finalOptions = { ...import_devtools_protocol4.DEFAULT_JSON_SORT_OPTIONS, ...options };
599
+ const finalOptions = { ...import_devtools_protocol5.DEFAULT_JSON_SORT_OPTIONS, ...options };
455
600
  const json = parseJson(text);
456
601
  const sorted = sortObject(json, finalOptions);
457
602
  return JSON.stringify(sorted, null, detectIndent(text));
@@ -513,9 +658,6 @@ function sortKeys(keys, options) {
513
658
  case "alphaNum":
514
659
  compareFn = (a, b) => a.localeCompare(b, void 0, { numeric: true });
515
660
  break;
516
- case "values":
517
- case "type":
518
- case "default":
519
661
  default:
520
662
  compareFn = (a, b) => a.localeCompare(b);
521
663
  break;
@@ -559,8 +701,10 @@ function formatArgs(args) {
559
701
  function createConsoleLogger(prefix = "serviceme") {
560
702
  return {
561
703
  debug(message, ...args) {
562
- process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}
563
- `);
704
+ process.stderr.write(
705
+ `[${prefix}] DEBUG ${message} ${formatArgs(args)}
706
+ `
707
+ );
564
708
  },
565
709
  info(message, ...args) {
566
710
  process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
@@ -571,401 +715,18 @@ function createConsoleLogger(prefix = "serviceme") {
571
715
  `);
572
716
  },
573
717
  error(message, ...args) {
574
- process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}
575
- `);
718
+ process.stderr.write(
719
+ `[${prefix}] ERROR ${message} ${formatArgs(args)}
720
+ `
721
+ );
576
722
  }
577
723
  };
578
724
  }
579
725
 
580
- // src/opencode/OpenCodeManager.ts
581
- var import_node_child_process2 = require("child_process");
582
- var import_node_crypto = require("crypto");
583
- var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
584
- var DEFAULT_OPTIONS = {
585
- command: "opencode",
586
- installUrl: "https://opencode.ai",
587
- host: "127.0.0.1",
588
- portMin: 16384,
589
- portMax: 65535,
590
- healthPath: "/app",
591
- sessionPath: "/session",
592
- sessionStatusPath: "/session/status",
593
- startupPollIntervalMs: 200,
594
- idleTimeoutMs: 10 * 60 * 1e3,
595
- stdoutLogLimitBytes: 64 * 1024
596
- };
597
- var OpenCodeManager = class {
598
- constructor(options = {}) {
599
- this.stdoutBytes = 0;
600
- this.options = {
601
- ...DEFAULT_OPTIONS,
602
- ...options
603
- };
604
- this.logger = options.logger ?? noopLogger;
605
- }
606
- async isAvailable() {
607
- return commandExists(this.options.command);
608
- }
609
- isRunning() {
610
- return Boolean(this.childProcess && !this.childProcess.killed && this.port);
611
- }
612
- async ensureServer(runtime = {}) {
613
- await this.ensureServerStarted(runtime);
614
- return this.getServerState();
615
- }
616
- async getServerState() {
617
- const available = await this.isAvailable();
618
- const running = this.isRunning();
619
- const healthy = this.port ? await this.isServerHealthy(this.port) : false;
620
- return {
621
- available,
622
- running,
623
- healthy,
624
- installUrl: this.options.installUrl,
625
- pid: this.childProcess?.pid,
626
- port: this.port
627
- };
628
- }
629
- async createSession(runtime = {}) {
630
- await this.ensureServerStarted(runtime);
631
- const session = await this.request(
632
- this.options.sessionPath,
633
- {
634
- method: "POST",
635
- query: this.createQuery(runtime.workspaceRoot),
636
- body: {}
637
- }
638
- );
639
- this.resetIdleTimer();
640
- return {
641
- sessionId: session.id,
642
- title: session.title,
643
- status: "running"
644
- };
645
- }
646
- async prompt(input) {
647
- await this.ensureServerStarted({ workspaceRoot: input.workspaceRoot });
648
- await this.request(
649
- `${this.options.sessionPath}/${input.sessionId}/message`,
650
- {
651
- method: "POST",
652
- query: this.createQuery(input.workspaceRoot),
653
- body: {
654
- parts: [
655
- {
656
- type: "text",
657
- text: input.prompt
658
- }
659
- ]
660
- },
661
- signal: input.signal
662
- }
663
- );
664
- const latestMessage = await this.getLatestAssistantMessage(
665
- input.sessionId,
666
- input.workspaceRoot,
667
- input.signal
668
- );
669
- input.onEvent?.({
670
- type: "status",
671
- status: "running"
672
- });
673
- if (latestMessage) {
674
- input.onEvent?.({
675
- type: "message",
676
- text: latestMessage
677
- });
678
- }
679
- input.onEvent?.({
680
- type: "completed"
681
- });
682
- this.resetIdleTimer();
683
- }
684
- async getSessionStatus(sessionId, workspaceRoot) {
685
- await this.ensureServerStarted({ workspaceRoot });
686
- const response = await this.request(this.options.sessionStatusPath, {
687
- method: "GET",
688
- query: this.createQuery(workspaceRoot)
689
- });
690
- const session = response.find((item) => item.id === sessionId);
691
- this.resetIdleTimer();
692
- return this.mapStatus(session?.status);
693
- }
694
- async abortSession(sessionId, workspaceRoot) {
695
- await this.ensureServerStarted({ workspaceRoot });
696
- await this.request(`${this.options.sessionPath}/${sessionId}/abort`, {
697
- method: "POST",
698
- query: this.createQuery(workspaceRoot)
699
- });
700
- this.resetIdleTimer();
701
- }
702
- async dispose() {
703
- if (this.idleTimer) {
704
- clearTimeout(this.idleTimer);
705
- this.idleTimer = void 0;
706
- }
707
- await this.terminateTrackedProcess("dispose");
708
- this.startupPromise = void 0;
709
- }
710
- async ensureServerStarted(runtime) {
711
- if (this.port) {
712
- if (await this.isServerHealthy(this.port)) {
713
- return;
714
- }
715
- await this.terminateTrackedProcess("restart-unhealthy");
716
- }
717
- if (this.startupPromise) {
718
- await this.startupPromise;
719
- return;
720
- }
721
- if (!await this.isAvailable()) {
722
- throw (0, import_devtools_protocol6.createServicemeError)(
723
- "opencode_not_installed",
724
- "OpenCode CLI is not installed."
725
- );
726
- }
727
- const MAX_PORT_RETRIES = 3;
728
- let lastError;
729
- for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
730
- this.startupPromise = this.startServerProcess(runtime);
731
- try {
732
- await this.startupPromise;
733
- return;
734
- } catch (error) {
735
- lastError = error;
736
- this.logger.warn(
737
- `OpenCode server start attempt ${String(attempt + 1)} failed, retrying...`
738
- );
739
- } finally {
740
- this.startupPromise = void 0;
741
- }
742
- }
743
- throw lastError;
744
- }
745
- async startServerProcess(runtime) {
746
- const port = (0, import_node_crypto.randomInt)(this.options.portMin, this.options.portMax + 1);
747
- const child = (0, import_node_child_process2.spawn)(this.options.command, ["--port", String(port)], {
748
- cwd: runtime.workspaceRoot,
749
- detached: process.platform !== "win32",
750
- env: {
751
- ...process.env,
752
- OPENCODE_CALLER: "serviceme-cli"
753
- },
754
- stdio: ["ignore", "pipe", "pipe"]
755
- });
756
- this.stdoutBytes = 0;
757
- child.stdout?.on("data", (chunk) => {
758
- if (this.stdoutBytes < this.options.stdoutLogLimitBytes) {
759
- this.logger.debug("OpenCode stdout", chunk.toString());
760
- this.stdoutBytes += chunk.length;
761
- }
762
- });
763
- child.stderr?.on("data", (chunk) => {
764
- this.logger.warn("OpenCode stderr", chunk.toString());
765
- });
766
- child.on("error", (error) => {
767
- this.logger.error("OpenCode process error", error);
768
- if (this.childProcess === child) {
769
- this.childProcess = void 0;
770
- this.port = void 0;
771
- }
772
- });
773
- child.on("exit", () => {
774
- if (this.childProcess === child) {
775
- this.childProcess = void 0;
776
- this.port = void 0;
777
- }
778
- });
779
- if (process.platform !== "win32" && child.pid) {
780
- (0, import_node_child_process2.spawn)("renice", ["+10", String(child.pid)], { stdio: "ignore" }).unref();
781
- }
782
- this.childProcess = child;
783
- this.port = port;
784
- const ready = await this.waitForHealth(
785
- port,
786
- runtime.startupTimeoutMs ?? 5e3
787
- );
788
- if (!ready) {
789
- await this.terminateTrackedProcess("startup-timeout");
790
- throw (0, import_devtools_protocol6.createServicemeError)(
791
- "opencode_startup_timeout",
792
- "OpenCode local server did not start in time."
793
- );
794
- }
795
- }
796
- async waitForHealth(port, timeoutMs) {
797
- const startedAt = Date.now();
798
- while (Date.now() - startedAt < timeoutMs) {
799
- if (await this.isServerHealthy(port)) {
800
- return true;
801
- }
802
- await new Promise((resolve) => {
803
- setTimeout(resolve, this.options.startupPollIntervalMs);
804
- });
805
- }
806
- return false;
807
- }
808
- async isServerHealthy(port) {
809
- try {
810
- const response = await fetch(
811
- `http://${this.options.host}:${port}${this.options.healthPath}`
812
- );
813
- return response.ok;
814
- } catch {
815
- return false;
816
- }
817
- }
818
- resetIdleTimer() {
819
- if (this.idleTimer) {
820
- clearTimeout(this.idleTimer);
821
- }
822
- this.idleTimer = setTimeout(() => {
823
- void this.dispose();
824
- }, this.options.idleTimeoutMs);
825
- this.idleTimer.unref?.();
826
- }
827
- async terminateTrackedProcess(reason) {
828
- const child = this.childProcess;
829
- this.childProcess = void 0;
830
- this.port = void 0;
831
- if (!child || child.killed) {
832
- return;
833
- }
834
- this.logger.info("Stopping tracked OpenCode process", {
835
- reason,
836
- pid: child.pid
837
- });
838
- if (process.platform === "win32") {
839
- if (child.pid) {
840
- await new Promise((resolve) => {
841
- const killProcess = (0, import_node_child_process2.spawn)(
842
- "taskkill",
843
- ["/pid", String(child.pid), "/T", "/F"],
844
- {
845
- stdio: "ignore",
846
- windowsHide: true
847
- }
848
- );
849
- killProcess.once("exit", () => resolve());
850
- killProcess.once("error", () => {
851
- child.kill();
852
- resolve();
853
- });
854
- });
855
- return;
856
- }
857
- child.kill();
858
- return;
859
- }
860
- if (!child.pid) {
861
- child.kill("SIGTERM");
862
- return;
863
- }
864
- try {
865
- process.kill(-child.pid, "SIGTERM");
866
- } catch {
867
- child.kill("SIGTERM");
868
- return;
869
- }
870
- const childPid = child.pid;
871
- await new Promise((resolve) => {
872
- const escalationTimer = setTimeout(() => {
873
- try {
874
- process.kill(-childPid, "SIGKILL");
875
- } catch {
876
- }
877
- resolve();
878
- }, 2e3);
879
- child.once("exit", () => {
880
- clearTimeout(escalationTimer);
881
- try {
882
- process.kill(-childPid, "SIGKILL");
883
- } catch {
884
- }
885
- resolve();
886
- });
887
- });
888
- }
889
- async request(path3, options) {
890
- if (!this.port) {
891
- throw (0, import_devtools_protocol6.createServicemeError)(
892
- "opencode_backend_not_running",
893
- "OpenCode local server is not running."
894
- );
895
- }
896
- const url = new URL(`http://${this.options.host}:${this.port}${path3}`);
897
- for (const [key, value] of Object.entries(options.query ?? {})) {
898
- if (value) {
899
- url.searchParams.set(key, value);
900
- }
901
- }
902
- const response = await fetch(url, {
903
- method: options.method,
904
- headers: {
905
- "Content-Type": "application/json"
906
- },
907
- body: options.body ? JSON.stringify(options.body) : void 0,
908
- signal: options.signal
909
- });
910
- if (!response.ok) {
911
- throw (0, import_devtools_protocol6.createServicemeError)(
912
- "opencode_request_failed",
913
- `OpenCode request failed: ${response.status} ${response.statusText}`
914
- );
915
- }
916
- if (response.status === 204) {
917
- return void 0;
918
- }
919
- return await response.json();
920
- }
921
- createQuery(workspaceRoot) {
922
- return {
923
- directory: workspaceRoot
924
- };
925
- }
926
- async getLatestAssistantMessage(sessionId, workspaceRoot, signal) {
927
- const messages = await this.request(
928
- `${this.options.sessionPath}/${sessionId}/message`,
929
- {
930
- method: "GET",
931
- query: {
932
- ...this.createQuery(workspaceRoot),
933
- limit: "20"
934
- },
935
- signal
936
- }
937
- );
938
- const latestAssistant = [...messages].reverse().find((message) => message.info?.role === "assistant");
939
- if (!latestAssistant?.parts) {
940
- return void 0;
941
- }
942
- return latestAssistant.parts.filter(
943
- (part) => part.type === "text" && typeof part.text === "string"
944
- ).map((part) => part.text).join("\n\n");
945
- }
946
- mapStatus(status) {
947
- switch (status) {
948
- case "running":
949
- case "active":
950
- return "running";
951
- case "needs_input":
952
- return "needs-input";
953
- case "completed":
954
- return "completed";
955
- case "failed":
956
- return "failed";
957
- case "aborted":
958
- return "aborted";
959
- default:
960
- return "idle";
961
- }
962
- }
963
- };
964
-
965
726
  // src/project/projectTools.ts
966
727
  var fs2 = __toESM(require("fs/promises"));
967
728
  var path2 = __toESM(require("path"));
968
- var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
729
+ var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
969
730
 
970
731
  // src/utils/fileUtils.ts
971
732
  var import_node_fs = require("fs");
@@ -974,39 +735,49 @@ var import_node_path = require("path");
974
735
  var import_yauzl = require("yauzl");
975
736
  var unzipFile = (zipPath, dest) => {
976
737
  return new Promise((resolve, reject) => {
977
- (0, import_yauzl.open)(zipPath, { lazyEntries: true }, (err, zipfile) => {
978
- if (err) return reject(err);
979
- if (!zipfile) return reject(new Error("Failed to open zip file."));
980
- zipfile.readEntry();
981
- zipfile.on("entry", (entry) => {
982
- if (/\/$/.test(entry.fileName)) {
983
- void (0, import_promises.mkdir)((0, import_node_path.join)(dest, entry.fileName), { recursive: true }).then(() => {
984
- zipfile.readEntry();
985
- }).catch(reject);
986
- } else {
987
- const outputPath = (0, import_node_path.join)(dest, entry.fileName);
988
- void (0, import_promises.mkdir)((0, import_node_path.dirname)(outputPath), { recursive: true }).then(() => {
989
- zipfile.openReadStream(entry, (streamError, readStream) => {
990
- if (streamError) return reject(streamError);
991
- if (!readStream) return reject(new Error("Failed to open zip entry stream."));
992
- const writeStream = (0, import_node_fs.createWriteStream)(outputPath);
993
- readStream.on("error", reject);
994
- writeStream.on("error", reject);
995
- writeStream.on("close", () => {
996
- zipfile.readEntry();
997
- });
998
- readStream.pipe(writeStream);
999
- });
1000
- }).catch(reject);
1001
- }
1002
- });
1003
- zipfile.on("end", () => {
1004
- resolve();
1005
- });
1006
- zipfile.on("error", (zipError) => {
1007
- reject(zipError);
1008
- });
1009
- });
738
+ (0, import_yauzl.open)(
739
+ zipPath,
740
+ { lazyEntries: true },
741
+ (err, zipfile) => {
742
+ if (err) return reject(err);
743
+ if (!zipfile) return reject(new Error("Failed to open zip file."));
744
+ zipfile.readEntry();
745
+ zipfile.on("entry", (entry) => {
746
+ if (/\/$/.test(entry.fileName)) {
747
+ void (0, import_promises.mkdir)((0, import_node_path.join)(dest, entry.fileName), { recursive: true }).then(() => {
748
+ zipfile.readEntry();
749
+ }).catch(reject);
750
+ } else {
751
+ const outputPath = (0, import_node_path.join)(dest, entry.fileName);
752
+ void (0, import_promises.mkdir)((0, import_node_path.dirname)(outputPath), { recursive: true }).then(() => {
753
+ zipfile.openReadStream(
754
+ entry,
755
+ (streamError, readStream) => {
756
+ if (streamError) return reject(streamError);
757
+ if (!readStream)
758
+ return reject(
759
+ new Error("Failed to open zip entry stream.")
760
+ );
761
+ const writeStream = (0, import_node_fs.createWriteStream)(outputPath);
762
+ readStream.on("error", reject);
763
+ writeStream.on("error", reject);
764
+ writeStream.on("close", () => {
765
+ zipfile.readEntry();
766
+ });
767
+ readStream.pipe(writeStream);
768
+ }
769
+ );
770
+ }).catch(reject);
771
+ }
772
+ });
773
+ zipfile.on("end", () => {
774
+ resolve();
775
+ });
776
+ zipfile.on("error", (zipError) => {
777
+ reject(zipError);
778
+ });
779
+ }
780
+ );
1010
781
  });
1011
782
  };
1012
783
  var tryLstat = async (targetPath) => {
@@ -1030,7 +801,11 @@ var mergeEntry = async (sourcePath, destPath, overwrite) => {
1030
801
  await (0, import_promises.mkdir)(destPath, { recursive: true });
1031
802
  const children = await (0, import_promises.readdir)(sourcePath);
1032
803
  for (const child of children) {
1033
- await mergeEntry((0, import_node_path.join)(sourcePath, child), (0, import_node_path.join)(destPath, child), overwrite);
804
+ await mergeEntry(
805
+ (0, import_node_path.join)(sourcePath, child),
806
+ (0, import_node_path.join)(destPath, child),
807
+ overwrite
808
+ );
1034
809
  }
1035
810
  await (0, import_promises.rm)(sourcePath, { recursive: true, force: true });
1036
811
  return;
@@ -1107,7 +882,7 @@ var ProjectTools = class {
1107
882
  }
1108
883
  async installDependencies(workspacePath, command) {
1109
884
  if (!command) {
1110
- throw (0, import_devtools_protocol7.createServicemeError)("invalid_params", "Expected install command.");
885
+ throw (0, import_devtools_protocol6.createServicemeError)("invalid_params", "Expected install command.");
1111
886
  }
1112
887
  await runCommand(command, {
1113
888
  cwd: workspacePath,
@@ -1170,7 +945,7 @@ var ProjectTools = class {
1170
945
  }
1171
946
  async runScaffoldPrune(workspacePath, preset) {
1172
947
  if (!preset) {
1173
- throw (0, import_devtools_protocol7.createServicemeError)("invalid_params", "Expected scaffold preset.");
948
+ throw (0, import_devtools_protocol6.createServicemeError)("invalid_params", "Expected scaffold preset.");
1174
949
  }
1175
950
  const commands = [
1176
951
  `pnpm run scaffold:prune -- --preset ${preset} --project-root .`,
@@ -1250,17 +1025,976 @@ var ProjectTools = class {
1250
1025
  function createProjectTools() {
1251
1026
  return new ProjectTools();
1252
1027
  }
1028
+
1029
+ // src/scheduled-tasks/daemon/DaemonLogger.ts
1030
+ var fs3 = __toESM(require("fs"));
1031
+ var path3 = __toESM(require("path"));
1032
+ var CONFIG_DIR = ".serviceme";
1033
+ var LOG_FILE = "scheduler.log";
1034
+ var MAX_LOG_SIZE = 1024 * 1024;
1035
+ var DaemonLogger = class {
1036
+ constructor(workspacePath) {
1037
+ this.logPath = path3.join(workspacePath, CONFIG_DIR, LOG_FILE);
1038
+ const dir = path3.dirname(this.logPath);
1039
+ if (!fs3.existsSync(dir)) {
1040
+ fs3.mkdirSync(dir, { recursive: true });
1041
+ }
1042
+ }
1043
+ getLogPath() {
1044
+ return this.logPath;
1045
+ }
1046
+ log(level, message) {
1047
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1048
+ const line = `[${ts}] [${level.toUpperCase()}] ${message}
1049
+ `;
1050
+ this.rotateIfNeeded();
1051
+ fs3.appendFileSync(this.logPath, line, "utf-8");
1052
+ }
1053
+ rotateIfNeeded() {
1054
+ try {
1055
+ const stats = fs3.statSync(this.logPath);
1056
+ if (stats.size > MAX_LOG_SIZE) {
1057
+ const content = fs3.readFileSync(this.logPath, "utf-8");
1058
+ const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
1059
+ if (halfIdx > 0) {
1060
+ fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1061
+ }
1062
+ }
1063
+ } catch {
1064
+ }
1065
+ }
1066
+ };
1067
+
1068
+ // src/scheduled-tasks/daemon/PidManager.ts
1069
+ var fs4 = __toESM(require("fs"));
1070
+ var path4 = __toESM(require("path"));
1071
+ var CONFIG_DIR2 = ".serviceme";
1072
+ var PID_FILE = "scheduler.pid";
1073
+ var PidManager = class {
1074
+ constructor(workspacePath) {
1075
+ this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
1076
+ }
1077
+ getPidPath() {
1078
+ return this.pidPath;
1079
+ }
1080
+ writePid(pid) {
1081
+ const dir = path4.dirname(this.pidPath);
1082
+ if (!fs4.existsSync(dir)) {
1083
+ fs4.mkdirSync(dir, { recursive: true });
1084
+ }
1085
+ fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
1086
+ }
1087
+ readPid() {
1088
+ if (!fs4.existsSync(this.pidPath)) return null;
1089
+ const raw = fs4.readFileSync(this.pidPath, "utf-8").trim();
1090
+ const pid = Number.parseInt(raw, 10);
1091
+ return Number.isNaN(pid) ? null : pid;
1092
+ }
1093
+ removePid() {
1094
+ if (fs4.existsSync(this.pidPath)) {
1095
+ fs4.unlinkSync(this.pidPath);
1096
+ }
1097
+ }
1098
+ isProcessRunning(pid) {
1099
+ try {
1100
+ process.kill(pid, 0);
1101
+ return true;
1102
+ } catch {
1103
+ return false;
1104
+ }
1105
+ }
1106
+ getRunningPid() {
1107
+ const pid = this.readPid();
1108
+ if (pid === null) return null;
1109
+ if (this.isProcessRunning(pid)) return pid;
1110
+ this.removePid();
1111
+ return null;
1112
+ }
1113
+ };
1114
+
1115
+ // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1116
+ var fs7 = __toESM(require("fs"));
1117
+
1118
+ // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1119
+ var import_node_child_process2 = require("child_process");
1120
+ var DEFAULT_TIMEOUT = 3e5;
1121
+ var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
1122
+ var GithubCopilotCliExecutor = class {
1123
+ async execute(payload, abortSignal) {
1124
+ const authenticated = await isCopilotAuthenticated();
1125
+ if (!authenticated) {
1126
+ return {
1127
+ status: "failure",
1128
+ error: "GitHub Copilot CLI authentication failed. Please run `gh auth login` to re-authenticate."
1129
+ };
1130
+ }
1131
+ let output = "";
1132
+ const handle = this.executeStreaming(
1133
+ payload,
1134
+ (_stream, data) => {
1135
+ output += data;
1136
+ },
1137
+ abortSignal
1138
+ );
1139
+ const result = await handle.result;
1140
+ return { ...result, output: output || result.output };
1141
+ }
1142
+ executeStreaming(payload, onOutput, abortSignal) {
1143
+ const p = payload;
1144
+ const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT;
1145
+ let resolve;
1146
+ const resultPromise = new Promise((r) => {
1147
+ resolve = r;
1148
+ });
1149
+ let settled = false;
1150
+ const settle = (result) => {
1151
+ if (settled) return;
1152
+ settled = true;
1153
+ clearTimeout(timer);
1154
+ resolve?.(result);
1155
+ };
1156
+ if (abortSignal?.aborted) {
1157
+ return {
1158
+ result: Promise.resolve({
1159
+ status: "cancelled",
1160
+ error: "Execution aborted"
1161
+ }),
1162
+ cancel: () => {
1163
+ }
1164
+ };
1165
+ }
1166
+ const args = ["copilot", "prompt", "--prompt", p.prompt];
1167
+ if (p.autopilot) {
1168
+ args.push("--autopilot");
1169
+ }
1170
+ if (p.allowTools && p.allowTools.length > 0) {
1171
+ args.push("--allow-tools", p.allowTools.join(","));
1172
+ }
1173
+ if (p.model) {
1174
+ args.push("--model", p.model);
1175
+ }
1176
+ if (p.agent) {
1177
+ args.push("--agent", p.agent);
1178
+ }
1179
+ if (p.timeout != null) {
1180
+ args.push("--timeout", String(p.timeout));
1181
+ }
1182
+ const child = (0, import_node_child_process2.spawn)("serviceme", args, {
1183
+ cwd: p.workspace,
1184
+ stdio: ["ignore", "pipe", "pipe"]
1185
+ });
1186
+ const timer = setTimeout(() => {
1187
+ child.kill("SIGTERM");
1188
+ setTimeout(() => {
1189
+ if (!child.killed) child.kill("SIGKILL");
1190
+ }, 5e3);
1191
+ settle({
1192
+ status: "timeout",
1193
+ error: `Copilot CLI execution timed out after ${timeout / 1e3}s`
1194
+ });
1195
+ }, timeout);
1196
+ let stdoutBuf = "";
1197
+ let stderrBuf = "";
1198
+ child.stdout.on("data", (chunk) => {
1199
+ const data = chunk.toString();
1200
+ stdoutBuf += data;
1201
+ if (stdoutBuf.length > MAX_OUTPUT_BYTES)
1202
+ stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES);
1203
+ onOutput("stdout", data);
1204
+ });
1205
+ child.stderr.on("data", (chunk) => {
1206
+ const data = chunk.toString();
1207
+ stderrBuf += data;
1208
+ if (stderrBuf.length > MAX_OUTPUT_BYTES)
1209
+ stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES);
1210
+ onOutput("stderr", data);
1211
+ });
1212
+ child.on("close", (code) => {
1213
+ if (code === 0) {
1214
+ settle({ status: "success", output: stdoutBuf || void 0 });
1215
+ } else {
1216
+ settle({
1217
+ status: "failure",
1218
+ output: stdoutBuf || void 0,
1219
+ error: stderrBuf || `Process exited with code ${code}`
1220
+ });
1221
+ }
1222
+ });
1223
+ child.on("error", (err) => {
1224
+ settle({ status: "failure", error: err.message });
1225
+ });
1226
+ const cancelFn = () => {
1227
+ child.kill("SIGTERM");
1228
+ setTimeout(() => {
1229
+ if (!child.killed) child.kill("SIGKILL");
1230
+ }, 5e3);
1231
+ settle({ status: "cancelled", error: "Execution cancelled" });
1232
+ };
1233
+ abortSignal?.addEventListener("abort", () => cancelFn(), { once: true });
1234
+ return { result: resultPromise, cancel: cancelFn };
1235
+ }
1236
+ };
1237
+
1238
+ // src/scheduled-tasks/executors/HttpRequestExecutor.ts
1239
+ var DEFAULT_TIMEOUT2 = 3e4;
1240
+ var HttpRequestExecutor = class {
1241
+ async execute(payload, abortSignal) {
1242
+ const p = payload;
1243
+ const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT2;
1244
+ const ac = new AbortController();
1245
+ let _timedOut = false;
1246
+ const timer = setTimeout(() => {
1247
+ _timedOut = true;
1248
+ ac.abort();
1249
+ }, timeout);
1250
+ if (abortSignal?.aborted) {
1251
+ clearTimeout(timer);
1252
+ return { status: "cancelled", error: "Execution aborted" };
1253
+ }
1254
+ let externalAbort = false;
1255
+ abortSignal?.addEventListener(
1256
+ "abort",
1257
+ () => {
1258
+ externalAbort = true;
1259
+ clearTimeout(timer);
1260
+ ac.abort();
1261
+ },
1262
+ { once: true }
1263
+ );
1264
+ try {
1265
+ const response = await fetch(p.url, {
1266
+ method: p.method,
1267
+ headers: p.headers,
1268
+ body: p.body,
1269
+ signal: ac.signal
1270
+ });
1271
+ clearTimeout(timer);
1272
+ const body = await response.text();
1273
+ if (response.ok) {
1274
+ return {
1275
+ status: "success",
1276
+ output: `${response.status} ${response.statusText}
1277
+ ${body}`.trim()
1278
+ };
1279
+ }
1280
+ return {
1281
+ status: "failure",
1282
+ error: `HTTP ${response.status} ${response.statusText}
1283
+ ${body}`.trim()
1284
+ };
1285
+ } catch (err) {
1286
+ clearTimeout(timer);
1287
+ if (err instanceof Error && err.name === "AbortError") {
1288
+ if (externalAbort) {
1289
+ return { status: "cancelled", error: "Execution cancelled" };
1290
+ }
1291
+ return {
1292
+ status: "timeout",
1293
+ error: `HTTP request timed out after ${timeout / 1e3}s`
1294
+ };
1295
+ }
1296
+ const message = err instanceof Error ? err.message : String(err);
1297
+ return { status: "failure", error: message };
1298
+ }
1299
+ }
1300
+ };
1301
+
1302
+ // src/scheduled-tasks/executors/ShellExecutor.ts
1303
+ var import_node_child_process3 = require("child_process");
1304
+ var DEFAULT_TIMEOUT3 = 6e4;
1305
+ var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1306
+ var ShellExecutor = class {
1307
+ async execute(payload, abortSignal) {
1308
+ let output = "";
1309
+ const handle = this.executeStreaming(
1310
+ payload,
1311
+ (_stream, data) => {
1312
+ output += data;
1313
+ },
1314
+ abortSignal
1315
+ );
1316
+ const result = await handle.result;
1317
+ return { ...result, output: output || result.output };
1318
+ }
1319
+ executeStreaming(payload, onOutput, abortSignal) {
1320
+ const p = payload;
1321
+ const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT3;
1322
+ let resolve;
1323
+ const resultPromise = new Promise((r) => {
1324
+ resolve = r;
1325
+ });
1326
+ let settled = false;
1327
+ const settle = (result) => {
1328
+ if (settled) return;
1329
+ settled = true;
1330
+ clearTimeout(timer);
1331
+ resolve?.(result);
1332
+ };
1333
+ if (abortSignal?.aborted) {
1334
+ return {
1335
+ result: Promise.resolve({
1336
+ status: "cancelled",
1337
+ error: "Execution aborted"
1338
+ }),
1339
+ cancel: () => {
1340
+ }
1341
+ };
1342
+ }
1343
+ const child = (0, import_node_child_process3.spawn)("sh", ["-c", p.script], {
1344
+ cwd: p.cwd,
1345
+ stdio: ["ignore", "pipe", "pipe"]
1346
+ });
1347
+ const timer = setTimeout(() => {
1348
+ child.kill("SIGTERM");
1349
+ setTimeout(() => {
1350
+ if (!child.killed) child.kill("SIGKILL");
1351
+ }, 5e3);
1352
+ settle({
1353
+ status: "timeout",
1354
+ error: `Shell execution timed out after ${timeout / 1e3}s`
1355
+ });
1356
+ }, timeout);
1357
+ let stdoutBuf = "";
1358
+ let stderrBuf = "";
1359
+ child.stdout.on("data", (chunk) => {
1360
+ const data = chunk.toString();
1361
+ stdoutBuf += data;
1362
+ if (stdoutBuf.length > MAX_OUTPUT_BYTES2)
1363
+ stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES2);
1364
+ onOutput("stdout", data);
1365
+ });
1366
+ child.stderr.on("data", (chunk) => {
1367
+ const data = chunk.toString();
1368
+ stderrBuf += data;
1369
+ if (stderrBuf.length > MAX_OUTPUT_BYTES2)
1370
+ stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES2);
1371
+ onOutput("stderr", data);
1372
+ });
1373
+ child.on("close", (code) => {
1374
+ if (code === 0) {
1375
+ settle({ status: "success", output: stdoutBuf || void 0 });
1376
+ } else {
1377
+ settle({
1378
+ status: "failure",
1379
+ output: stdoutBuf || void 0,
1380
+ error: stderrBuf || `Process exited with code ${code}`
1381
+ });
1382
+ }
1383
+ });
1384
+ child.on("error", (err) => {
1385
+ settle({ status: "failure", error: err.message });
1386
+ });
1387
+ const cancelFn = () => {
1388
+ child.kill("SIGTERM");
1389
+ setTimeout(() => {
1390
+ if (!child.killed) child.kill("SIGKILL");
1391
+ }, 5e3);
1392
+ settle({ status: "cancelled", error: "Execution cancelled" });
1393
+ };
1394
+ abortSignal?.addEventListener("abort", () => cancelFn(), { once: true });
1395
+ return { result: resultPromise, cancel: cancelFn };
1396
+ }
1397
+ };
1398
+
1399
+ // src/scheduled-tasks/executors/types.ts
1400
+ function isStreamingTaskExecutor(executor) {
1401
+ return "executeStreaming" in executor && typeof executor.executeStreaming === "function";
1402
+ }
1403
+
1404
+ // src/scheduled-tasks/executors/index.ts
1405
+ var executors = {
1406
+ shell: new ShellExecutor(),
1407
+ http_request: new HttpRequestExecutor(),
1408
+ github_copilot_cli: new GithubCopilotCliExecutor()
1409
+ };
1410
+ function getExecutor(taskType) {
1411
+ const executor = executors[taskType];
1412
+ if (!executor) {
1413
+ throw new Error(`Unsupported bridge task type: ${taskType}`);
1414
+ }
1415
+ return executor;
1416
+ }
1417
+
1418
+ // src/scheduled-tasks/TaskConfigManager.ts
1419
+ var import_node_crypto = require("crypto");
1420
+ var fs5 = __toESM(require("fs"));
1421
+ var path5 = __toESM(require("path"));
1422
+ var CONFIG_DIR3 = ".serviceme";
1423
+ var CONFIG_FILE = "scheduled-tasks.json";
1424
+ function emptyConfig() {
1425
+ return { version: 1, tasks: [] };
1426
+ }
1427
+ var TaskConfigManager = class {
1428
+ constructor(workspacePath) {
1429
+ this.configPath = path5.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1430
+ }
1431
+ getConfigPath() {
1432
+ return this.configPath;
1433
+ }
1434
+ readConfig() {
1435
+ if (!fs5.existsSync(this.configPath)) {
1436
+ return emptyConfig();
1437
+ }
1438
+ const raw = fs5.readFileSync(this.configPath, "utf-8");
1439
+ const parsed = JSON.parse(raw);
1440
+ return parsed;
1441
+ }
1442
+ writeConfig(config) {
1443
+ const dir = path5.dirname(this.configPath);
1444
+ if (!fs5.existsSync(dir)) {
1445
+ fs5.mkdirSync(dir, { recursive: true });
1446
+ }
1447
+ const tmp = `${this.configPath}.tmp`;
1448
+ fs5.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1449
+ fs5.renameSync(tmp, this.configPath);
1450
+ }
1451
+ listTasks() {
1452
+ return this.readConfig().tasks;
1453
+ }
1454
+ getTask(id) {
1455
+ return this.readConfig().tasks.find((t) => t.id === id);
1456
+ }
1457
+ getTaskByName(name) {
1458
+ return this.readConfig().tasks.find((t) => t.name === name);
1459
+ }
1460
+ createTask(input) {
1461
+ const config = this.readConfig();
1462
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1463
+ const task = {
1464
+ id: (0, import_node_crypto.randomUUID)(),
1465
+ name: input.name,
1466
+ description: input.description,
1467
+ enabled: input.enabled ?? true,
1468
+ scheduleType: input.scheduleType,
1469
+ schedule: input.schedule,
1470
+ taskType: input.taskType,
1471
+ payload: input.payload,
1472
+ createdAt: now,
1473
+ updatedAt: now
1474
+ };
1475
+ config.tasks.push(task);
1476
+ this.writeConfig(config);
1477
+ return task;
1478
+ }
1479
+ editTask(id, input) {
1480
+ const config = this.readConfig();
1481
+ const idx = config.tasks.findIndex((t) => t.id === id);
1482
+ if (idx === -1) {
1483
+ throw new Error(`Task '${id}' not found`);
1484
+ }
1485
+ const existing = config.tasks[idx];
1486
+ if (!existing) {
1487
+ throw new Error(`Task '${id}' not found`);
1488
+ }
1489
+ const updated = {
1490
+ ...existing,
1491
+ ...input.name !== void 0 && { name: input.name },
1492
+ ...input.description !== void 0 && {
1493
+ description: input.description
1494
+ },
1495
+ ...input.scheduleType !== void 0 && {
1496
+ scheduleType: input.scheduleType
1497
+ },
1498
+ ...input.schedule !== void 0 && { schedule: input.schedule },
1499
+ ...input.taskType !== void 0 && { taskType: input.taskType },
1500
+ ...input.payload !== void 0 && { payload: input.payload },
1501
+ ...input.enabled !== void 0 && { enabled: input.enabled },
1502
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1503
+ };
1504
+ config.tasks[idx] = updated;
1505
+ this.writeConfig(config);
1506
+ return updated;
1507
+ }
1508
+ deleteTask(id) {
1509
+ const config = this.readConfig();
1510
+ const idx = config.tasks.findIndex((t) => t.id === id);
1511
+ if (idx === -1) {
1512
+ throw new Error(`Task '${id}' not found`);
1513
+ }
1514
+ const removed = config.tasks[idx];
1515
+ if (!removed) {
1516
+ throw new Error(`Task '${id}' not found`);
1517
+ }
1518
+ config.tasks.splice(idx, 1);
1519
+ this.writeConfig(config);
1520
+ return removed;
1521
+ }
1522
+ toggleTask(id, enabled) {
1523
+ return this.editTask(id, { enabled });
1524
+ }
1525
+ };
1526
+
1527
+ // src/scheduled-tasks/TaskLogManager.ts
1528
+ var import_node_crypto2 = require("crypto");
1529
+ var fs6 = __toESM(require("fs"));
1530
+ var path6 = __toESM(require("path"));
1531
+ var CONFIG_DIR4 = ".serviceme";
1532
+ var LOG_FILE2 = "scheduled-tasks-log.json";
1533
+ var MAX_LOGS = 200;
1534
+ function emptyLogFile() {
1535
+ return { logs: [] };
1536
+ }
1537
+ function isValidLogEntry(entry) {
1538
+ if (!entry || typeof entry !== "object") return false;
1539
+ const e = entry;
1540
+ return typeof e.id === "string" && typeof e.taskId === "string" && typeof e.taskName === "string" && typeof e.startedAt === "string" && typeof e.finishedAt === "string" && typeof e.status === "string";
1541
+ }
1542
+ function validateAndRepairLogFile(raw) {
1543
+ if (!raw || typeof raw !== "object") {
1544
+ return emptyLogFile();
1545
+ }
1546
+ const file = raw;
1547
+ if (!Array.isArray(file.logs)) {
1548
+ return emptyLogFile();
1549
+ }
1550
+ const validLogs = file.logs.filter(isValidLogEntry);
1551
+ return { logs: validLogs };
1552
+ }
1553
+ var TaskLogManager = class {
1554
+ constructor(workspacePath) {
1555
+ this.logPath = path6.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1556
+ }
1557
+ getLogPath() {
1558
+ return this.logPath;
1559
+ }
1560
+ readLogFile() {
1561
+ if (!fs6.existsSync(this.logPath)) {
1562
+ return emptyLogFile();
1563
+ }
1564
+ try {
1565
+ const raw = fs6.readFileSync(this.logPath, "utf-8");
1566
+ const parsed = JSON.parse(raw);
1567
+ const file = validateAndRepairLogFile(parsed);
1568
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
1569
+ this.writeLogFile(file);
1570
+ }
1571
+ return file;
1572
+ } catch {
1573
+ this.backupCorruptedFile();
1574
+ const fresh = emptyLogFile();
1575
+ this.writeLogFile(fresh);
1576
+ return fresh;
1577
+ }
1578
+ }
1579
+ backupCorruptedFile() {
1580
+ try {
1581
+ if (fs6.existsSync(this.logPath)) {
1582
+ const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1583
+ fs6.copyFileSync(this.logPath, backupPath);
1584
+ }
1585
+ } catch {
1586
+ }
1587
+ }
1588
+ writeLogFile(file) {
1589
+ const dir = path6.dirname(this.logPath);
1590
+ if (!fs6.existsSync(dir)) {
1591
+ fs6.mkdirSync(dir, { recursive: true });
1592
+ }
1593
+ const tmp = `${this.logPath}.tmp`;
1594
+ fs6.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1595
+ fs6.renameSync(tmp, this.logPath);
1596
+ }
1597
+ appendLog(input) {
1598
+ const file = this.readLogFile();
1599
+ const log = {
1600
+ id: (0, import_node_crypto2.randomUUID)(),
1601
+ taskId: input.taskId,
1602
+ taskName: input.taskName,
1603
+ startedAt: input.startedAt,
1604
+ finishedAt: input.finishedAt,
1605
+ status: input.status,
1606
+ output: input.output,
1607
+ error: input.error
1608
+ };
1609
+ file.logs.push(log);
1610
+ if (file.logs.length > MAX_LOGS) {
1611
+ file.logs = file.logs.slice(file.logs.length - MAX_LOGS);
1612
+ }
1613
+ this.writeLogFile(file);
1614
+ return log;
1615
+ }
1616
+ getLogs(options) {
1617
+ const file = this.readLogFile();
1618
+ let logs = file.logs;
1619
+ if (options?.taskId) {
1620
+ logs = logs.filter((l) => l.taskId === options.taskId);
1621
+ }
1622
+ const total = logs.length;
1623
+ logs = logs.slice().reverse();
1624
+ if (options?.limit && options.limit > 0) {
1625
+ logs = logs.slice(0, options.limit);
1626
+ }
1627
+ return { logs, total };
1628
+ }
1629
+ clearLogs(taskId) {
1630
+ const file = this.readLogFile();
1631
+ const before = file.logs.length;
1632
+ if (taskId) {
1633
+ file.logs = file.logs.filter((l) => l.taskId !== taskId);
1634
+ } else {
1635
+ file.logs = [];
1636
+ }
1637
+ this.writeLogFile(file);
1638
+ return before - file.logs.length;
1639
+ }
1640
+ };
1641
+
1642
+ // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1643
+ var TICK_INTERVAL = 1e3;
1644
+ var MIN_SCHEDULE_INTERVAL = 1e3;
1645
+ var SchedulerDaemon = class {
1646
+ constructor(workspacePath) {
1647
+ this.tickTimer = null;
1648
+ this.watcher = null;
1649
+ this.running = false;
1650
+ this.startTime = 0;
1651
+ // Track last execution time and running state per task
1652
+ this.lastRun = /* @__PURE__ */ new Map();
1653
+ this.taskRunning = /* @__PURE__ */ new Set();
1654
+ this.workspacePath = workspacePath;
1655
+ this.configManager = new TaskConfigManager(workspacePath);
1656
+ this.logManager = new TaskLogManager(workspacePath);
1657
+ this.pidManager = new PidManager(workspacePath);
1658
+ this.logger = new DaemonLogger(workspacePath);
1659
+ }
1660
+ start() {
1661
+ if (this.running) return;
1662
+ this.running = true;
1663
+ this.startTime = Date.now();
1664
+ this.pidManager.writePid(process.pid);
1665
+ this.logger.log("info", `Scheduler daemon started (PID: ${process.pid})`);
1666
+ this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
1667
+ this.setupConfigWatch();
1668
+ process.on("SIGTERM", () => this.stop());
1669
+ process.on("SIGINT", () => this.stop());
1670
+ }
1671
+ stop() {
1672
+ if (!this.running) return;
1673
+ this.running = false;
1674
+ if (this.tickTimer) {
1675
+ clearInterval(this.tickTimer);
1676
+ this.tickTimer = null;
1677
+ }
1678
+ if (this.watcher) {
1679
+ this.watcher.close();
1680
+ this.watcher = null;
1681
+ }
1682
+ this.pidManager.removePid();
1683
+ this.logger.log("info", "Scheduler daemon stopped");
1684
+ process.exit(0);
1685
+ }
1686
+ setupConfigWatch() {
1687
+ const configPath = this.configManager.getConfigPath();
1688
+ const dir = configPath.substring(0, configPath.lastIndexOf("/"));
1689
+ try {
1690
+ if (fs7.existsSync(dir)) {
1691
+ this.watcher = fs7.watch(dir, (_eventType, filename) => {
1692
+ if (filename === "scheduled-tasks.json") {
1693
+ this.logger.log("info", "Config file changed, reconciling...");
1694
+ }
1695
+ });
1696
+ }
1697
+ } catch {
1698
+ this.logger.log("warn", "Could not watch config directory");
1699
+ }
1700
+ }
1701
+ tick() {
1702
+ const config = this.configManager.readConfig();
1703
+ const now = Date.now();
1704
+ for (const task of config.tasks) {
1705
+ if (!task.enabled) continue;
1706
+ if (this.taskRunning.has(task.id)) continue;
1707
+ const lastExec = this.lastRun.get(task.id) ?? 0;
1708
+ if (this.shouldRun(task, lastExec, now)) {
1709
+ this.executeTask(task, now);
1710
+ }
1711
+ }
1712
+ }
1713
+ shouldRun(task, lastExec, now) {
1714
+ if (task.scheduleType === "interval") {
1715
+ const intervalMs = parseIntervalMs(task.schedule);
1716
+ if (intervalMs < MIN_SCHEDULE_INTERVAL) return false;
1717
+ return now - lastExec >= intervalMs;
1718
+ }
1719
+ if (task.scheduleType === "cron") {
1720
+ if (now - lastExec < 6e4) return false;
1721
+ return matchesCron(task.schedule, new Date(now));
1722
+ }
1723
+ return false;
1724
+ }
1725
+ async executeTask(task, now) {
1726
+ this.taskRunning.add(task.id);
1727
+ this.lastRun.set(task.id, now);
1728
+ const startedAt = new Date(now).toISOString();
1729
+ this.logger.log("info", `Executing task: ${task.name} (${task.id})`);
1730
+ try {
1731
+ const executor = getExecutor(task.taskType);
1732
+ const result = await executor.execute(task.payload);
1733
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1734
+ this.logManager.appendLog({
1735
+ taskId: task.id,
1736
+ taskName: task.name,
1737
+ startedAt,
1738
+ finishedAt,
1739
+ status: result.status === "running" ? "failure" : result.status,
1740
+ output: result.output,
1741
+ error: result.error
1742
+ });
1743
+ this.logger.log("info", `Task ${task.name} completed: ${result.status}`);
1744
+ } catch (err) {
1745
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1746
+ const message = err instanceof Error ? err.message : String(err);
1747
+ this.logManager.appendLog({
1748
+ taskId: task.id,
1749
+ taskName: task.name,
1750
+ startedAt,
1751
+ finishedAt,
1752
+ status: "failure",
1753
+ error: message
1754
+ });
1755
+ this.logger.log("error", `Task ${task.name} failed: ${message}`);
1756
+ } finally {
1757
+ this.taskRunning.delete(task.id);
1758
+ }
1759
+ }
1760
+ getStatus() {
1761
+ const config = this.configManager.readConfig();
1762
+ return {
1763
+ running: this.running,
1764
+ pid: process.pid,
1765
+ uptimeSeconds: this.running ? Math.floor((Date.now() - this.startTime) / 1e3) : null,
1766
+ tasksRegistered: config.tasks.length,
1767
+ tasksEnabled: config.tasks.filter((t) => t.enabled).length,
1768
+ workspacePath: this.workspacePath,
1769
+ pidFile: this.pidManager.getPidPath()
1770
+ };
1771
+ }
1772
+ };
1773
+ function parseIntervalMs(schedule) {
1774
+ const match = /^every\s+(\d+)\s*(s|sec|m|min|h|hr|d|day)s?$/i.exec(schedule);
1775
+ if (!match) return 0;
1776
+ const [, numStr, unit] = match;
1777
+ const num = Number.parseInt(numStr ?? "0", 10);
1778
+ switch (unit?.toLowerCase()) {
1779
+ case "s":
1780
+ case "sec":
1781
+ return num * 1e3;
1782
+ case "m":
1783
+ case "min":
1784
+ return num * 60 * 1e3;
1785
+ case "h":
1786
+ case "hr":
1787
+ return num * 60 * 60 * 1e3;
1788
+ case "d":
1789
+ case "day":
1790
+ return num * 24 * 60 * 60 * 1e3;
1791
+ default:
1792
+ return 0;
1793
+ }
1794
+ }
1795
+ function matchesCron(expression, date) {
1796
+ const parts = expression.trim().split(/\s+/);
1797
+ if (parts.length < 5) return false;
1798
+ const [minPart, hourPart, dayPart, monthPart, weekdayPart] = parts;
1799
+ if (!minPart || !hourPart || !dayPart || !monthPart || !weekdayPart)
1800
+ return false;
1801
+ return matchCronField(minPart, date.getMinutes()) && matchCronField(hourPart, date.getHours()) && matchCronField(dayPart, date.getDate()) && matchCronField(monthPart, date.getMonth() + 1) && matchCronField(weekdayPart, date.getDay());
1802
+ }
1803
+ function matchCronField(field, value) {
1804
+ if (field === "*") return true;
1805
+ if (field.startsWith("*/")) {
1806
+ const step = Number.parseInt(field.slice(2), 10);
1807
+ return step > 0 && value % step === 0;
1808
+ }
1809
+ const values = field.split(",");
1810
+ return values.some((v) => Number.parseInt(v, 10) === value);
1811
+ }
1812
+
1813
+ // src/scheduled-tasks/TaskExecutionEngine.ts
1814
+ var TaskExecutionEngine = class {
1815
+ constructor(getExecutor2) {
1816
+ this.getExecutor = getExecutor2;
1817
+ this.running = /* @__PURE__ */ new Map();
1818
+ this.taskExecutions = /* @__PURE__ */ new Map();
1819
+ this.listener = null;
1820
+ }
1821
+ setListener(listener) {
1822
+ this.listener = listener;
1823
+ }
1824
+ async execute(snapshot) {
1825
+ const policy = snapshot.concurrencyPolicy ?? "reject";
1826
+ if (policy === "reject") {
1827
+ const existingIds = this.taskExecutions.get(snapshot.taskId);
1828
+ if (existingIds && existingIds.size > 0) {
1829
+ throw new Error(
1830
+ `TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
1831
+ );
1832
+ }
1833
+ }
1834
+ const executor = this.getExecutor(snapshot.type);
1835
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1836
+ if (!this.taskExecutions.has(snapshot.taskId)) {
1837
+ this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
1838
+ }
1839
+ this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
1840
+ const ac = new AbortController();
1841
+ const runningExec = {
1842
+ executionId: snapshot.executionId,
1843
+ taskId: snapshot.taskId,
1844
+ startedAt,
1845
+ cancel: () => ac.abort()
1846
+ };
1847
+ this.running.set(snapshot.executionId, runningExec);
1848
+ this.listener?.onStarted({
1849
+ executionId: snapshot.executionId,
1850
+ taskId: snapshot.taskId,
1851
+ startedAt
1852
+ });
1853
+ try {
1854
+ let result;
1855
+ if (isStreamingTaskExecutor(executor)) {
1856
+ const handle = executor.executeStreaming(
1857
+ snapshot.payload,
1858
+ (stream, data) => {
1859
+ this.listener?.onOutput({
1860
+ executionId: snapshot.executionId,
1861
+ stream,
1862
+ data
1863
+ });
1864
+ },
1865
+ ac.signal
1866
+ );
1867
+ runningExec.cancel = () => {
1868
+ handle.cancel();
1869
+ ac.abort();
1870
+ };
1871
+ result = await handle.result;
1872
+ } else {
1873
+ result = await executor.execute(snapshot.payload, ac.signal);
1874
+ }
1875
+ const log = {
1876
+ id: snapshot.executionId,
1877
+ taskId: snapshot.taskId,
1878
+ taskName: snapshot.name,
1879
+ startedAt,
1880
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1881
+ status: result.status === "running" ? "failure" : result.status,
1882
+ output: result.output,
1883
+ error: result.error
1884
+ };
1885
+ switch (log.status) {
1886
+ case "success":
1887
+ this.listener?.onCompleted({
1888
+ executionId: snapshot.executionId,
1889
+ log
1890
+ });
1891
+ break;
1892
+ case "cancelled":
1893
+ this.listener?.onCancelled({
1894
+ executionId: snapshot.executionId,
1895
+ log
1896
+ });
1897
+ break;
1898
+ case "timeout":
1899
+ this.listener?.onFailed({
1900
+ executionId: snapshot.executionId,
1901
+ status: "timeout",
1902
+ reason: "timeout",
1903
+ log
1904
+ });
1905
+ break;
1906
+ default:
1907
+ this.listener?.onFailed({
1908
+ executionId: snapshot.executionId,
1909
+ status: "failure",
1910
+ reason: "error",
1911
+ log
1912
+ });
1913
+ break;
1914
+ }
1915
+ } catch (err) {
1916
+ const log = {
1917
+ id: snapshot.executionId,
1918
+ taskId: snapshot.taskId,
1919
+ taskName: snapshot.name,
1920
+ startedAt,
1921
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1922
+ status: ac.signal.aborted ? "cancelled" : "failure",
1923
+ error: err instanceof Error ? err.message : String(err)
1924
+ };
1925
+ if (ac.signal.aborted) {
1926
+ this.listener?.onCancelled({
1927
+ executionId: snapshot.executionId,
1928
+ log
1929
+ });
1930
+ } else {
1931
+ this.listener?.onFailed({
1932
+ executionId: snapshot.executionId,
1933
+ status: "failure",
1934
+ reason: "error",
1935
+ log
1936
+ });
1937
+ }
1938
+ } finally {
1939
+ this.running.delete(snapshot.executionId);
1940
+ const taskExecIds = this.taskExecutions.get(snapshot.taskId);
1941
+ if (taskExecIds) {
1942
+ taskExecIds.delete(snapshot.executionId);
1943
+ if (taskExecIds.size === 0) {
1944
+ this.taskExecutions.delete(snapshot.taskId);
1945
+ }
1946
+ }
1947
+ }
1948
+ }
1949
+ cancel(executionId) {
1950
+ const exec = this.running.get(executionId);
1951
+ if (!exec) return false;
1952
+ exec.cancel();
1953
+ return true;
1954
+ }
1955
+ listRunning() {
1956
+ return Array.from(this.running.values()).map((e) => ({
1957
+ executionId: e.executionId,
1958
+ taskId: e.taskId,
1959
+ startedAt: e.startedAt
1960
+ }));
1961
+ }
1962
+ dispose() {
1963
+ for (const exec of this.running.values()) {
1964
+ exec.cancel();
1965
+ }
1966
+ this.running.clear();
1967
+ this.taskExecutions.clear();
1968
+ }
1969
+ };
1253
1970
  // Annotate the CommonJS export names for ESM import in node:
1254
1971
  0 && (module.exports = {
1972
+ DaemonLogger,
1255
1973
  EnvironmentInspector,
1974
+ GithubCopilotCliExecutor,
1975
+ HttpRequestExecutor,
1256
1976
  ImageTools,
1257
- OpenCodeManager,
1977
+ PidManager,
1258
1978
  ProjectTools,
1979
+ SchedulerDaemon,
1980
+ ShellExecutor,
1981
+ TaskConfigManager,
1982
+ TaskExecutionEngine,
1983
+ TaskLogManager,
1984
+ copilotDoctor,
1985
+ copilotPrompt,
1259
1986
  createConsoleLogger,
1987
+ createCopilotAuthRequiredError,
1988
+ createCopilotNotInstalledError,
1260
1989
  createImageTools,
1261
1990
  createJsonTools,
1262
1991
  createProjectTools,
1992
+ getExecutor,
1993
+ isCopilotAuthenticated,
1994
+ isStreamingTaskExecutor,
1995
+ matchesCron,
1263
1996
  moveFiles,
1264
1997
  noopLogger,
1998
+ parseIntervalMs,
1265
1999
  unzipFile
1266
2000
  });