mcp-copilot-cli 1.0.0

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.
Files changed (79) hide show
  1. package/README.md +207 -0
  2. package/bin/mcp-copilot-cli.mjs +3 -0
  3. package/dist/src/app.d.ts +71 -0
  4. package/dist/src/app.js +303 -0
  5. package/dist/src/app.js.map +1 -0
  6. package/dist/src/cli/doctor.d.ts +2 -0
  7. package/dist/src/cli/doctor.js +99 -0
  8. package/dist/src/cli/doctor.js.map +1 -0
  9. package/dist/src/config/defaults.d.ts +3 -0
  10. package/dist/src/config/defaults.js +4 -0
  11. package/dist/src/config/defaults.js.map +1 -0
  12. package/dist/src/index.d.ts +2 -0
  13. package/dist/src/index.js +98 -0
  14. package/dist/src/index.js.map +1 -0
  15. package/dist/src/mcp/system-status.d.ts +19 -0
  16. package/dist/src/mcp/system-status.js +20 -0
  17. package/dist/src/mcp/system-status.js.map +1 -0
  18. package/dist/src/mcp/task-markdown.d.ts +3 -0
  19. package/dist/src/mcp/task-markdown.js +70 -0
  20. package/dist/src/mcp/task-markdown.js.map +1 -0
  21. package/dist/src/mcp/tool-banners.d.ts +4 -0
  22. package/dist/src/mcp/tool-banners.js +26 -0
  23. package/dist/src/mcp/tool-banners.js.map +1 -0
  24. package/dist/src/mcp/tool-definitions.d.ts +109 -0
  25. package/dist/src/mcp/tool-definitions.js +125 -0
  26. package/dist/src/mcp/tool-definitions.js.map +1 -0
  27. package/dist/src/services/copilot-runtime.d.ts +28 -0
  28. package/dist/src/services/copilot-runtime.js +194 -0
  29. package/dist/src/services/copilot-runtime.js.map +1 -0
  30. package/dist/src/services/output-log.d.ts +7 -0
  31. package/dist/src/services/output-log.js +59 -0
  32. package/dist/src/services/output-log.js.map +1 -0
  33. package/dist/src/services/profile-manager.d.ts +34 -0
  34. package/dist/src/services/profile-manager.js +113 -0
  35. package/dist/src/services/profile-manager.js.map +1 -0
  36. package/dist/src/services/question-registry.d.ts +29 -0
  37. package/dist/src/services/question-registry.js +115 -0
  38. package/dist/src/services/question-registry.js.map +1 -0
  39. package/dist/src/services/spawn-validation.d.ts +9 -0
  40. package/dist/src/services/spawn-validation.js +53 -0
  41. package/dist/src/services/spawn-validation.js.map +1 -0
  42. package/dist/src/services/task-manager.d.ts +34 -0
  43. package/dist/src/services/task-manager.js +107 -0
  44. package/dist/src/services/task-manager.js.map +1 -0
  45. package/dist/src/services/task-persistence.d.ts +20 -0
  46. package/dist/src/services/task-persistence.js +67 -0
  47. package/dist/src/services/task-persistence.js.map +1 -0
  48. package/dist/src/services/task-store.d.ts +12 -0
  49. package/dist/src/services/task-store.js +167 -0
  50. package/dist/src/services/task-store.js.map +1 -0
  51. package/dist/src/services/workspace-isolation.d.ts +13 -0
  52. package/dist/src/services/workspace-isolation.js +74 -0
  53. package/dist/src/services/workspace-isolation.js.map +1 -0
  54. package/dist/src/templates/agent-prompt.d.ts +6 -0
  55. package/dist/src/templates/agent-prompt.js +58 -0
  56. package/dist/src/templates/agent-prompt.js.map +1 -0
  57. package/dist/src/types/task.d.ts +79 -0
  58. package/dist/src/types/task.js +22 -0
  59. package/dist/src/types/task.js.map +1 -0
  60. package/package.json +63 -0
  61. package/src/app.ts +341 -0
  62. package/src/cli/doctor.ts +112 -0
  63. package/src/config/defaults.ts +3 -0
  64. package/src/index.ts +128 -0
  65. package/src/mcp/system-status.ts +41 -0
  66. package/src/mcp/task-markdown.ts +81 -0
  67. package/src/mcp/tool-banners.ts +32 -0
  68. package/src/mcp/tool-definitions.ts +151 -0
  69. package/src/services/copilot-runtime.ts +247 -0
  70. package/src/services/output-log.ts +68 -0
  71. package/src/services/profile-manager.ts +165 -0
  72. package/src/services/question-registry.ts +169 -0
  73. package/src/services/spawn-validation.ts +75 -0
  74. package/src/services/task-manager.ts +144 -0
  75. package/src/services/task-persistence.ts +100 -0
  76. package/src/services/task-store.ts +207 -0
  77. package/src/services/workspace-isolation.ts +100 -0
  78. package/src/templates/agent-prompt.ts +71 -0
  79. package/src/types/task.ts +95 -0
@@ -0,0 +1,58 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const ROLE_TEMPLATES = {
3
+ coder: [
4
+ 'You are the implementation agent.',
5
+ 'Work carefully, change as little as needed, and verify the requested behavior.',
6
+ 'Prefer deterministic edits over speculative refactors.',
7
+ ].join(' '),
8
+ planner: [
9
+ 'You are the planning agent.',
10
+ 'Clarify scope, constraints, risks, and the concrete implementation path.',
11
+ 'Produce actionable implementation guidance instead of vague advice.',
12
+ ].join(' '),
13
+ researcher: [
14
+ 'You are the research agent.',
15
+ 'Investigate the code and runtime behavior before proposing conclusions.',
16
+ 'Surface the facts that a downstream implementer needs.',
17
+ ].join(' '),
18
+ tester: [
19
+ 'You are the testing agent.',
20
+ 'Exercise the real behavior, collect evidence, and describe failures concretely.',
21
+ 'Prefer end-to-end verification when possible.',
22
+ ].join(' '),
23
+ general: [
24
+ 'You are a general-purpose autonomous agent.',
25
+ 'Stay focused on the requested deliverable and keep your output concrete.',
26
+ ].join(' '),
27
+ };
28
+ async function renderContextFiles(contextFiles) {
29
+ if (contextFiles.length === 0) {
30
+ return '';
31
+ }
32
+ const sections = await Promise.all(contextFiles.map(async (contextFile) => {
33
+ const content = await readFile(contextFile.path, 'utf8');
34
+ return [
35
+ `## Context File: ${contextFile.path}`,
36
+ contextFile.description ? `Description: ${contextFile.description}` : null,
37
+ '',
38
+ '```text',
39
+ content,
40
+ '```',
41
+ ].filter(Boolean).join('\n');
42
+ }));
43
+ return sections.join('\n\n');
44
+ }
45
+ export async function buildAgentPrompt(input) {
46
+ const context = await renderContextFiles(input.contextFiles);
47
+ return [
48
+ ROLE_TEMPLATES[input.agentType],
49
+ '',
50
+ 'Treat the task as stateful background work. If you need user input, ask a concise question.',
51
+ '',
52
+ '## Assignment',
53
+ input.prompt,
54
+ context ? '' : null,
55
+ context || null,
56
+ ].filter(Boolean).join('\n');
57
+ }
58
+ //# sourceMappingURL=agent-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-prompt.js","sourceRoot":"","sources":["../../../src/templates/agent-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5C,MAAM,cAAc,GAA8B;IAChD,KAAK,EAAE;QACL,mCAAmC;QACnC,gFAAgF;QAChF,wDAAwD;KACzD,CAAC,IAAI,CAAC,GAAG,CAAC;IACX,OAAO,EAAE;QACP,6BAA6B;QAC7B,0EAA0E;QAC1E,qEAAqE;KACtE,CAAC,IAAI,CAAC,GAAG,CAAC;IACX,UAAU,EAAE;QACV,6BAA6B;QAC7B,yEAAyE;QACzE,wDAAwD;KACzD,CAAC,IAAI,CAAC,GAAG,CAAC;IACX,MAAM,EAAE;QACN,4BAA4B;QAC5B,iFAAiF;QACjF,+CAA+C;KAChD,CAAC,IAAI,CAAC,GAAG,CAAC;IACX,OAAO,EAAE;QACP,6CAA6C;QAC7C,0EAA0E;KAC3E,CAAC,IAAI,CAAC,GAAG,CAAC;CACZ,CAAC;AAEF,KAAK,UAAU,kBAAkB,CAAC,YAA2B;IAC3D,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzD,OAAO;YACL,oBAAoB,WAAW,CAAC,IAAI,EAAE;YACtC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,gBAAgB,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;YAC1E,EAAE;YACF,SAAS;YACT,OAAO;YACP,KAAK;SACN,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAItC;IACC,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAE7D,OAAO;QACL,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC;QAC/B,EAAE;QACF,6FAA6F;QAC7F,EAAE;QACF,eAAe;QACf,KAAK,CAAC,MAAM;QACZ,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI;QACnB,OAAO,IAAI,IAAI;KAChB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,79 @@
1
+ export type AgentType = 'coder' | 'planner' | 'researcher' | 'tester' | 'general';
2
+ export interface ContextFile {
3
+ path: string;
4
+ description?: string | undefined;
5
+ }
6
+ export interface PendingQuestion {
7
+ question: string;
8
+ choices?: string[] | undefined;
9
+ allowFreeform: boolean;
10
+ askedAt: string;
11
+ sessionId: string;
12
+ }
13
+ export declare enum TaskStatus {
14
+ PENDING = "pending",
15
+ WAITING = "waiting",
16
+ RUNNING = "running",
17
+ WAITING_ANSWER = "waiting_answer",
18
+ COMPLETED = "completed",
19
+ FAILED = "failed",
20
+ CANCELLED = "cancelled",
21
+ RATE_LIMITED = "rate_limited",
22
+ TIMED_OUT = "timed_out"
23
+ }
24
+ export declare const TERMINAL_STATUSES: Set<TaskStatus>;
25
+ export declare function isTerminalStatus(status: TaskStatus): boolean;
26
+ export interface TaskRecord {
27
+ id: string;
28
+ status: TaskStatus;
29
+ prompt: string;
30
+ cwd: string;
31
+ model: string;
32
+ agentType: AgentType;
33
+ output: string[];
34
+ startTime: string;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ labels: string[];
38
+ contextFiles: ContextFile[];
39
+ dependsOn?: string[] | undefined;
40
+ error?: string | undefined;
41
+ endTime?: string | undefined;
42
+ outputFilePath?: string | undefined;
43
+ timeoutMs?: number | undefined;
44
+ sessionId?: string | undefined;
45
+ parentTaskId?: string | undefined;
46
+ pendingQuestion?: PendingQuestion | undefined;
47
+ profileId?: string | undefined;
48
+ profileConfigDir?: string | undefined;
49
+ isolationMode?: 'shared' | 'isolated' | undefined;
50
+ baseCwd?: string | undefined;
51
+ }
52
+ export interface CreateTaskInput {
53
+ prompt: string;
54
+ cwd: string;
55
+ model: string;
56
+ agentType: AgentType;
57
+ labels?: string[] | undefined;
58
+ contextFiles?: ContextFile[] | undefined;
59
+ dependsOn?: string[] | undefined;
60
+ timeoutMs?: number | undefined;
61
+ sessionId?: string | undefined;
62
+ parentTaskId?: string | undefined;
63
+ profileId?: string | undefined;
64
+ profileConfigDir?: string | undefined;
65
+ isolationMode?: 'shared' | 'isolated' | undefined;
66
+ baseCwd?: string | undefined;
67
+ }
68
+ export interface PersistedWorkspaceState {
69
+ version: 1;
70
+ tasks: TaskRecord[];
71
+ profiles: PersistedProfileState[];
72
+ }
73
+ export interface PersistedProfileState {
74
+ id: string;
75
+ configDir: string;
76
+ cooldownUntil?: number | undefined;
77
+ failureCount: number;
78
+ lastFailureReason?: string | undefined;
79
+ }
@@ -0,0 +1,22 @@
1
+ export var TaskStatus;
2
+ (function (TaskStatus) {
3
+ TaskStatus["PENDING"] = "pending";
4
+ TaskStatus["WAITING"] = "waiting";
5
+ TaskStatus["RUNNING"] = "running";
6
+ TaskStatus["WAITING_ANSWER"] = "waiting_answer";
7
+ TaskStatus["COMPLETED"] = "completed";
8
+ TaskStatus["FAILED"] = "failed";
9
+ TaskStatus["CANCELLED"] = "cancelled";
10
+ TaskStatus["RATE_LIMITED"] = "rate_limited";
11
+ TaskStatus["TIMED_OUT"] = "timed_out";
12
+ })(TaskStatus || (TaskStatus = {}));
13
+ export const TERMINAL_STATUSES = new Set([
14
+ TaskStatus.COMPLETED,
15
+ TaskStatus.FAILED,
16
+ TaskStatus.CANCELLED,
17
+ TaskStatus.TIMED_OUT,
18
+ ]);
19
+ export function isTerminalStatus(status) {
20
+ return TERMINAL_STATUSES.has(status);
21
+ }
22
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","sourceRoot":"","sources":["../../../src/types/task.ts"],"names":[],"mappings":"AAeA,MAAM,CAAN,IAAY,UAUX;AAVD,WAAY,UAAU;IACpB,iCAAmB,CAAA;IACnB,iCAAmB,CAAA;IACnB,iCAAmB,CAAA;IACnB,+CAAiC,CAAA;IACjC,qCAAuB,CAAA;IACvB,+BAAiB,CAAA;IACjB,qCAAuB,CAAA;IACvB,2CAA6B,CAAA;IAC7B,qCAAuB,CAAA;AACzB,CAAC,EAVW,UAAU,KAAV,UAAU,QAUrB;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAa;IACnD,UAAU,CAAC,SAAS;IACpB,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,SAAS;IACpB,UAAU,CAAC,SAAS;CACrB,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IACjD,OAAO,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACvC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "mcp-copilot-cli",
3
+ "version": "1.0.0",
4
+ "description": "Copilot-only MCP server CLI with resumable background agents",
5
+ "type": "module",
6
+ "main": "dist/src/index.js",
7
+ "bin": {
8
+ "mcp-copilot-cli": "bin/mcp-copilot-cli.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist/src",
13
+ "src"
14
+ ],
15
+ "directories": {
16
+ "test": "test"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "clean": "rm -rf dist",
21
+ "dev": "tsx watch src/index.ts",
22
+ "serve": "node --import tsx src/index.ts",
23
+ "doctor": "node --import tsx src/cli/doctor.ts",
24
+ "prepack": "npm run build",
25
+ "smoke": "node --import tsx scripts/smoke.ts",
26
+ "test": "npm run test:unit",
27
+ "test:unit": "node --import tsx --test test/**/*.test.ts",
28
+ "test:smoke": "node --import tsx scripts/smoke.ts"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "copilot",
33
+ "agent",
34
+ "sdk"
35
+ ],
36
+ "author": "Yigit Konur",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/yigitkonur/mcp-copilot-cli.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/yigitkonur/mcp-copilot-cli/issues"
44
+ },
45
+ "homepage": "https://github.com/yigitkonur/mcp-copilot-cli#readme",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.0.0"
51
+ },
52
+ "dependencies": {
53
+ "@github/copilot-sdk": "^0.1.30",
54
+ "@modelcontextprotocol/sdk": "^1.0.0",
55
+ "tsx": "^4.19.2",
56
+ "unique-names-generator": "^4.7.1",
57
+ "zod": "^3.24.1"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.10.5",
61
+ "typescript": "^5.7.2"
62
+ }
63
+ }
package/src/app.ts ADDED
@@ -0,0 +1,341 @@
1
+ import { OutputLogService } from './services/output-log.js';
2
+ import { ProfileManager } from './services/profile-manager.js';
3
+ import { QuestionRegistry } from './services/question-registry.js';
4
+ import { TaskManager } from './services/task-manager.js';
5
+ import { CopilotRuntime } from './services/copilot-runtime.js';
6
+ import { prepareTaskWorkspace } from './services/workspace-isolation.js';
7
+ import { DEFAULT_ISOLATION_MODE, DEFAULT_MODEL, DEFAULT_TIMEOUT_MS } from './config/defaults.js';
8
+ import { buildAnswerBanner, buildMessageBanner } from './mcp/tool-banners.js';
9
+ import { createToolDefinitions, type AnswerAgentInput, type CancelAgentInput, type MessageAgentInput, type SpawnAgentInput } from './mcp/tool-definitions.js';
10
+ import { renderTaskListMarkdown, renderTaskMarkdown } from './mcp/task-markdown.js';
11
+ import { buildSystemStatus } from './mcp/system-status.js';
12
+ import type { ContextFile, TaskRecord } from './types/task.js';
13
+ import { TaskStatus } from './types/task.js';
14
+ import { validateSpawnAgentInput } from './services/spawn-validation.js';
15
+
16
+ export class CopilotMcpApp {
17
+ readonly outputLog = new OutputLogService();
18
+ readonly profileManager = ProfileManager.fromEnvironment();
19
+ readonly taskManager = new TaskManager({
20
+ outputLog: this.outputLog,
21
+ getProfiles: () => this.profileManager.toPersistedState(),
22
+ });
23
+ readonly questionRegistry = new QuestionRegistry(this.taskManager);
24
+ readonly runtime = new CopilotRuntime({
25
+ taskManager: this.taskManager,
26
+ profileManager: this.profileManager,
27
+ questionRegistry: this.questionRegistry,
28
+ });
29
+
30
+ async initialize(cwd: string): Promise<void> {
31
+ const restored = await this.taskManager.load(cwd);
32
+ this.profileManager.hydrate(restored.profiles);
33
+ }
34
+
35
+ listTools() {
36
+ return createToolDefinitions({
37
+ messageBanner: buildMessageBanner(this.taskManager),
38
+ answerBanner: buildAnswerBanner(this.questionRegistry),
39
+ });
40
+ }
41
+
42
+ async spawnAgent(input: SpawnAgentInput): Promise<string> {
43
+ const baseCwd = input.cwd ?? process.cwd();
44
+ const validated = await validateSpawnAgentInput({ input, baseCwd });
45
+ const isolationMode = input.isolation_mode ?? DEFAULT_ISOLATION_MODE;
46
+ const workspace = await prepareTaskWorkspace({
47
+ baseCwd,
48
+ taskId: `${Date.now().toString(36)}-workspace`,
49
+ isolationMode,
50
+ });
51
+
52
+ const task = await this.taskManager.createTask({
53
+ prompt: input.prompt,
54
+ cwd: workspace.cwd,
55
+ baseCwd,
56
+ model: input.model ?? DEFAULT_MODEL,
57
+ agentType: input.agent_type,
58
+ labels: input.labels,
59
+ contextFiles: validated.contextFiles,
60
+ dependsOn: input.depends_on,
61
+ timeoutMs: input.timeout ?? DEFAULT_TIMEOUT_MS,
62
+ isolationMode,
63
+ });
64
+
65
+ if (task.status === TaskStatus.WAITING) {
66
+ return [
67
+ '✅ **Task queued (waiting for dependencies)**',
68
+ `task_id: \`${task.id}\``,
69
+ task.outputFilePath ? `output_file: \`${task.outputFilePath}\`` : null,
70
+ '',
71
+ '**Monitor:** Read `task:///all` — shows all tasks with status and pending questions in one table.',
72
+ ].filter(Boolean).join('\n');
73
+ }
74
+
75
+ void this.startTask(task.id).catch(() => {});
76
+
77
+ return [
78
+ '✅ **Task launched**',
79
+ `task_id: \`${task.id}\``,
80
+ task.outputFilePath ? `read logs: \`cat -n ${task.outputFilePath}\`` : null,
81
+ task.outputFilePath ? 'Use `cat -n` to read with line numbers, then on subsequent reads use `tail -n +<N>` to skip already-read lines.' : null,
82
+ '',
83
+ '**What to do next:**',
84
+ '- If you still have more agents to launch, launch them now — all agents run in parallel.',
85
+ '- Once all agents are launched, run `sleep 30` and then check status.',
86
+ `- To check status, read the MCP resource \`task:///${task.id}\` — it will show current progress, output, and whether the agent needs input.`,
87
+ task.outputFilePath ? `- For a quick progress check without reading the full resource, run \`wc -l ${task.outputFilePath}\` — a growing line count means the agent is still working.` : null,
88
+ ].filter(Boolean).join('\n');
89
+ }
90
+
91
+ async messageAgent(input: MessageAgentInput): Promise<string> {
92
+ const original = this.taskManager.getTask(input.task_id);
93
+ if (!original) {
94
+ throw new Error(`Task not found: ${input.task_id}`);
95
+ }
96
+ if (!original.sessionId) {
97
+ throw new Error(`Task ${input.task_id} has no resumable session`);
98
+ }
99
+
100
+ const task = await this.taskManager.createTask({
101
+ prompt: input.message ?? 'continue',
102
+ cwd: input.cwd ?? original.cwd,
103
+ baseCwd: input.cwd ?? original.baseCwd ?? original.cwd,
104
+ model: original.model,
105
+ agentType: original.agentType,
106
+ labels: [...original.labels, `continued-from:${original.id}`],
107
+ contextFiles: original.contextFiles,
108
+ timeoutMs: input.timeout ?? original.timeoutMs ?? DEFAULT_TIMEOUT_MS,
109
+ sessionId: original.sessionId,
110
+ parentTaskId: original.id,
111
+ profileId: original.profileId,
112
+ profileConfigDir: original.profileConfigDir,
113
+ isolationMode: original.isolationMode,
114
+ });
115
+
116
+ void this.startTask(task.id).catch(() => {});
117
+
118
+ return [
119
+ '✅ **Message sent**',
120
+ `task_id: \`${task.id}\``,
121
+ '',
122
+ task.outputFilePath ? `read logs: \`cat -n ${task.outputFilePath}\`` : null,
123
+ task.outputFilePath ? 'Use `cat -n` to read with line numbers, then on subsequent reads use `tail -n +<N>` to skip already-read lines.' : null,
124
+ ].filter(Boolean).join('\n');
125
+ }
126
+
127
+ async cancelAgent(input: CancelAgentInput): Promise<string> {
128
+ if (input.task_id === 'all') {
129
+ if (!input.clear || !input.confirm) {
130
+ throw new Error('Use clear=true and confirm=true with task_id="all"');
131
+ }
132
+
133
+ const count = await this.taskManager.clearAllTasks();
134
+ return `✅ Cleared **${count}** tasks from workspace.`;
135
+ }
136
+
137
+ const taskIds = Array.isArray(input.task_id) ? input.task_id : [input.task_id];
138
+ const results = await Promise.all(taskIds.map((taskId) => this.taskManager.cancelTask(taskId)));
139
+ return `✅ Cancelled ${results.filter(Boolean).length}/${taskIds.length} task(s).`;
140
+ }
141
+
142
+ async answerAgent(input: AnswerAgentInput): Promise<string> {
143
+ const rawAnswer = input.answer ?? Object.values(input.answers ?? {})[0];
144
+ if (!rawAnswer) {
145
+ throw new Error('Provide answer or answers');
146
+ }
147
+
148
+ const result = this.questionRegistry.submitAnswer(input.task_id, rawAnswer);
149
+ if (!result.success) {
150
+ throw new Error(result.error);
151
+ }
152
+
153
+ return [
154
+ '✅ **Answer submitted**',
155
+ `task_id: \`${input.task_id}\``,
156
+ `**Answer:** ${result.resolvedAnswer}`,
157
+ ].join('\n');
158
+ }
159
+
160
+ listResources() {
161
+ return [
162
+ {
163
+ uri: 'system:///status',
164
+ name: 'System Status',
165
+ description: 'Current task and profile counts',
166
+ mimeType: 'application/json',
167
+ },
168
+ {
169
+ uri: 'task:///all',
170
+ name: 'All Tasks',
171
+ description: `${this.taskManager.getAllTasks().length} tracked tasks`,
172
+ mimeType: 'text/markdown',
173
+ },
174
+ ...this.taskManager.getAllTasks().flatMap((task) => ([
175
+ {
176
+ uri: `task:///${task.id}`,
177
+ name: task.id,
178
+ description: task.status,
179
+ mimeType: 'text/markdown',
180
+ },
181
+ {
182
+ uri: `task:///${task.id}/session`,
183
+ name: `${task.id} session`,
184
+ description: `Execution log for ${task.id}`,
185
+ mimeType: 'application/json',
186
+ },
187
+ ])),
188
+ ];
189
+ }
190
+
191
+ async readResource(uri: string): Promise<{ mimeType: string; text: string }> {
192
+ if (uri === 'system:///status') {
193
+ return {
194
+ mimeType: 'application/json',
195
+ text: JSON.stringify(
196
+ buildSystemStatus({
197
+ tasks: this.taskManager.getAllTasks(),
198
+ profiles: this.profileManager.getProfiles(),
199
+ }),
200
+ null,
201
+ 2,
202
+ ),
203
+ };
204
+ }
205
+
206
+ if (uri === 'task:///all') {
207
+ return {
208
+ mimeType: 'text/markdown',
209
+ text: renderTaskListMarkdown(this.taskManager.getAllTasks()),
210
+ };
211
+ }
212
+
213
+ if (uri.startsWith('task:///') && uri.endsWith('/session')) {
214
+ const taskId = uri.slice('task:///'.length, -'/session'.length);
215
+ const task = this.getTaskOrThrow(taskId);
216
+ return {
217
+ mimeType: 'application/json',
218
+ text: JSON.stringify({
219
+ taskId: task.id,
220
+ status: task.status,
221
+ output: task.output,
222
+ }),
223
+ };
224
+ }
225
+
226
+ if (uri.startsWith('task:///')) {
227
+ const taskId = uri.slice('task:///'.length);
228
+ const task = this.getTaskOrThrow(taskId);
229
+ return {
230
+ mimeType: 'text/markdown',
231
+ text: renderTaskMarkdown(task),
232
+ };
233
+ }
234
+
235
+ throw new Error(`Unknown resource URI: ${uri}`);
236
+ }
237
+
238
+ async shutdown(): Promise<void> {
239
+ await this.runtime.shutdown();
240
+ }
241
+
242
+ listTaskSummaries() {
243
+ return this.taskManager.getAllTasks().map((task) => this.toMcpTask(task));
244
+ }
245
+
246
+ getTaskSummary(taskId: string) {
247
+ return this.toMcpTask(this.getTaskOrThrow(taskId));
248
+ }
249
+
250
+ getTaskPayload(taskId: string) {
251
+ const task = this.getTaskOrThrow(taskId);
252
+ return {
253
+ taskId: task.id,
254
+ status: task.status,
255
+ output: task.output,
256
+ error: task.error,
257
+ outputFilePath: task.outputFilePath,
258
+ pendingQuestion: task.pendingQuestion,
259
+ };
260
+ }
261
+
262
+ async cancelTaskById(taskId: string) {
263
+ const cancelled = await this.taskManager.cancelTask(taskId);
264
+ if (!cancelled && !this.taskManager.getTask(taskId)) {
265
+ throw new Error(`Task not found: ${taskId}`);
266
+ }
267
+ return this.getTaskSummary(taskId);
268
+ }
269
+
270
+ private async startTask(taskId: string): Promise<void> {
271
+ const task = this.taskManager.getTask(taskId);
272
+ if (!task) {
273
+ return;
274
+ }
275
+
276
+ try {
277
+ await this.runtime.runTask(task);
278
+ const ready = await this.taskManager.releaseReadyTasks();
279
+ await Promise.all(ready.map((released) => this.startTask(released.id)));
280
+ } catch (error) {
281
+ await this.taskManager.updateTask(taskId, {
282
+ status: TaskStatus.FAILED,
283
+ error: error instanceof Error ? error.message : String(error),
284
+ endTime: new Date().toISOString(),
285
+ }).catch(() => {});
286
+ }
287
+ }
288
+
289
+ private getTaskOrThrow(taskId: string) {
290
+ const task = this.taskManager.getTask(taskId);
291
+ if (!task) {
292
+ throw new Error(`Task not found: ${taskId}`);
293
+ }
294
+ return task;
295
+ }
296
+
297
+ private toMcpTask(task: TaskRecord) {
298
+ return {
299
+ taskId: task.id,
300
+ status: this.toMcpTaskStatus(task.status),
301
+ statusMessage: this.buildTaskStatusMessage(task),
302
+ createdAt: task.createdAt,
303
+ lastUpdatedAt: task.updatedAt,
304
+ ttl: task.timeoutMs ?? null,
305
+ pollInterval: 2_000,
306
+ };
307
+ }
308
+
309
+ private toMcpTaskStatus(status: TaskStatus): 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled' {
310
+ switch (status) {
311
+ case TaskStatus.WAITING_ANSWER:
312
+ return 'input_required';
313
+ case TaskStatus.COMPLETED:
314
+ return 'completed';
315
+ case TaskStatus.CANCELLED:
316
+ return 'cancelled';
317
+ case TaskStatus.FAILED:
318
+ case TaskStatus.TIMED_OUT:
319
+ case TaskStatus.RATE_LIMITED:
320
+ return 'failed';
321
+ default:
322
+ return 'working';
323
+ }
324
+ }
325
+
326
+ private buildTaskStatusMessage(task: TaskRecord): string | undefined {
327
+ if (task.pendingQuestion?.question) {
328
+ return task.pendingQuestion.question;
329
+ }
330
+ if (task.status === TaskStatus.WAITING && (task.dependsOn?.length ?? 0) > 0) {
331
+ return `Waiting for dependencies: ${(task.dependsOn ?? []).join(', ')}`;
332
+ }
333
+ if (task.error) {
334
+ return task.error;
335
+ }
336
+ if (task.status === TaskStatus.COMPLETED) {
337
+ return 'Task completed';
338
+ }
339
+ return undefined;
340
+ }
341
+ }
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process';
4
+ import process from 'node:process';
5
+ import { promisify } from 'node:util';
6
+
7
+ import { CopilotClient } from '@github/copilot-sdk';
8
+
9
+ import { buildCopilotClientOptions } from '../services/copilot-runtime.js';
10
+ import { ProfileManager } from '../services/profile-manager.js';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ async function commandVersion(command: string, args: string[] = ['--version']): Promise<string | null> {
15
+ try {
16
+ const { stdout, stderr } = await execFileAsync(command, args, { encoding: 'utf8' });
17
+ return (stdout || stderr).trim() || null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ async function inspectProfiles() {
24
+ const profileManager = ProfileManager.fromEnvironment();
25
+ const profiles = profileManager.getProfiles();
26
+
27
+ return Promise.all(
28
+ profiles.map(async (profile) => {
29
+ const client = new CopilotClient(
30
+ buildCopilotClientOptions({ cwd: process.cwd(), profile }),
31
+ );
32
+
33
+ try {
34
+ await client.start();
35
+ const auth = await client.getAuthStatus();
36
+ const models = auth.isAuthenticated
37
+ ? await client.listModels().catch(() => [])
38
+ : [];
39
+
40
+ return {
41
+ id: profile.id,
42
+ configDir: profile.configDir,
43
+ authenticated: auth.isAuthenticated,
44
+ login: auth.login,
45
+ models: models.slice(0, 10).map((model) => model.id),
46
+ };
47
+ } catch (error) {
48
+ return {
49
+ id: profile.id,
50
+ configDir: profile.configDir,
51
+ authenticated: false,
52
+ error: error instanceof Error ? error.message : String(error),
53
+ models: [],
54
+ };
55
+ } finally {
56
+ await client.stop().catch(async () => {
57
+ await client.forceStop().catch(() => {});
58
+ });
59
+ }
60
+ }),
61
+ );
62
+ }
63
+
64
+ async function main(): Promise<void> {
65
+ const json = process.argv.includes('--json');
66
+ const inspectedProfiles = await inspectProfiles();
67
+ const report = {
68
+ node: process.version,
69
+ copilot: await commandVersion('copilot'),
70
+ mcpc: await commandVersion('mcpc'),
71
+ cwd: process.cwd(),
72
+ copilotCliPath: buildCopilotClientOptions({
73
+ cwd: process.cwd(),
74
+ profile: {
75
+ id: 'doctor',
76
+ configDir: process.env.COPILOT_CONFIG_DIRS?.split(',')[0]?.trim() || `${process.env.HOME}/.copilot`,
77
+ failureCount: 0,
78
+ },
79
+ }).cliPath,
80
+ profiles: inspectedProfiles,
81
+ };
82
+
83
+ if (json) {
84
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
85
+ return;
86
+ }
87
+
88
+ process.stdout.write(`Node: ${report.node}\n`);
89
+ process.stdout.write(`Copilot CLI: ${report.copilot ?? 'not found'}\n`);
90
+ process.stdout.write(`Copilot CLI Path: ${report.copilotCliPath}\n`);
91
+ process.stdout.write(`mcpc: ${report.mcpc ?? 'not found'}\n`);
92
+ process.stdout.write(`cwd: ${report.cwd}\n`);
93
+ process.stdout.write('\nProfiles:\n');
94
+ for (const profile of report.profiles) {
95
+ process.stdout.write(`- ${profile.id} (${profile.configDir}) authenticated=${String(profile.authenticated)}`);
96
+ if ('login' in profile && profile.login) {
97
+ process.stdout.write(` login=${profile.login}`);
98
+ }
99
+ if ('error' in profile && profile.error) {
100
+ process.stdout.write(` error=${profile.error}`);
101
+ }
102
+ process.stdout.write('\n');
103
+ if (profile.models.length > 0) {
104
+ process.stdout.write(` models: ${profile.models.join(', ')}\n`);
105
+ }
106
+ }
107
+ }
108
+
109
+ main().catch((error) => {
110
+ console.error(error);
111
+ process.exit(1);
112
+ });
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_MODEL = 'gpt-5.4';
2
+ export const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
3
+ export const DEFAULT_ISOLATION_MODE = 'isolated';