@serviceme/devtools-core 0.0.6 → 0.1.2

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