copilot-operator 0.1.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.
- package/LICENSE +21 -0
- package/README.md +556 -0
- package/dist/src/api/app.module.js +18 -0
- package/dist/src/api/dataAcl.js +109 -0
- package/dist/src/api/folderPicker.js +110 -0
- package/dist/src/api/main.js +132 -0
- package/dist/src/api/operator.controller.js +975 -0
- package/dist/src/api/operator.service.js +1971 -0
- package/dist/src/api/saveToDesktop.js +157 -0
- package/dist/src/api/security.js +225 -0
- package/dist/src/api/settings.js +57 -0
- package/dist/src/cli.js +531 -0
- package/dist/src/config/layout.js +66 -0
- package/dist/src/config/lockedPolicy.js +176 -0
- package/dist/src/config/schema.js +487 -0
- package/dist/src/context/contextFiles.js +295 -0
- package/dist/src/context/desktopMirror.js +113 -0
- package/dist/src/context/projectMirror.js +364 -0
- package/dist/src/exec/authorizer.js +61 -0
- package/dist/src/exec/checks.js +285 -0
- package/dist/src/exec/confinement.js +224 -0
- package/dist/src/exec/dangerous.js +161 -0
- package/dist/src/exec/environment.js +80 -0
- package/dist/src/exec/isolation.js +166 -0
- package/dist/src/exec/network.js +242 -0
- package/dist/src/exec/policy.js +201 -0
- package/dist/src/exec/policyManifest.js +84 -0
- package/dist/src/exec/processes.js +329 -0
- package/dist/src/exec/programs.js +290 -0
- package/dist/src/exec/redaction.js +93 -0
- package/dist/src/exec/reportFile.js +154 -0
- package/dist/src/exec/runner.js +245 -0
- package/dist/src/exec/scriptFiles.js +81 -0
- package/dist/src/exec/shellExecuteTrap.js +221 -0
- package/dist/src/exec/shells.js +298 -0
- package/dist/src/exec/stepEnv.js +72 -0
- package/dist/src/exec/winps.js +17 -0
- package/dist/src/exec/workDir.js +94 -0
- package/dist/src/log/runLog.js +43 -0
- package/dist/src/orchestrator/derivedChecks.js +95 -0
- package/dist/src/orchestrator/review.js +590 -0
- package/dist/src/orchestrator/taskRunner.js +1582 -0
- package/dist/src/plan/brief.js +1785 -0
- package/dist/src/plan/importPlan.js +202 -0
- package/dist/src/plan/schema.js +483 -0
- package/dist/src/plan/script.js +1012 -0
- package/dist/src/plan/systemGuide.js +1851 -0
- package/dist/src/protocol/parser.js +224 -0
- package/dist/src/protocol/replySchema.js +207 -0
- package/dist/src/protocol/reporter.js +75 -0
- package/dist/src/protocol/reviewSchema.js +205 -0
- package/dist/src/session/compose.js +67 -0
- package/dist/src/session/debugExport.js +165 -0
- package/dist/src/session/events.js +34 -0
- package/dist/src/session/exportRecord.js +261 -0
- package/dist/src/session/exports.js +484 -0
- package/dist/src/session/model.js +22 -0
- package/dist/src/session/retention.js +34 -0
- package/dist/src/session/runName.js +36 -0
- package/dist/src/session/store.js +440 -0
- package/dist/src/session/story.js +198 -0
- package/dist/src/transport/acceptance.js +34 -0
- package/dist/src/transport/chatSession.js +73 -0
- package/dist/src/transport/copilotTransport.js +1208 -0
- package/dist/src/transport/edgeCrash.js +59 -0
- package/dist/src/transport/locators.js +261 -0
- package/dist/src/transport/profileLock.js +102 -0
- package/dist/src/util/pacing.js +89 -0
- package/dist/src/vcs/commitHygiene.js +102 -0
- package/dist/src/vcs/git.js +286 -0
- package/dist/src/vcs/taskVcs.js +343 -0
- package/dist/web/404.html +15 -0
- package/dist/web/_next/static/chunks/12.f1f4d35b52633e8d.js +1 -0
- package/dist/web/_next/static/chunks/131-bae98f790a541679.js +1 -0
- package/dist/web/_next/static/chunks/197-f7c33a0114fc3db3.js +1 -0
- package/dist/web/_next/static/chunks/489.158bbe303a595fba.js +1 -0
- package/dist/web/_next/static/chunks/643-34b9f66a6c317c4a.js +1 -0
- package/dist/web/_next/static/chunks/867-03b7cac8f79896c3.js +1 -0
- package/dist/web/_next/static/chunks/955-63de06e182d98949.js +1 -0
- package/dist/web/_next/static/chunks/app/_not-found/page-0d0d8e7e62efca12.js +1 -0
- package/dist/web/_next/static/chunks/app/appearance/page-501842e7aeac7f6b.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-ed858127cbdfb4ab.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-bad4f3e89b485719.js +1 -0
- package/dist/web/_next/static/chunks/app/import/page-db1f5f1ab2f5cad1.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-72251c52cc54f95e.js +1 -0
- package/dist/web/_next/static/chunks/app/level1/page-cf3e50cf64fecf4f.js +1 -0
- package/dist/web/_next/static/chunks/app/page-6fd16e5c8cf3b20f.js +1 -0
- package/dist/web/_next/static/chunks/app/presets/page-951e78b80e0e22e6.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-f2e63618097d7221.js +1 -0
- package/dist/web/_next/static/chunks/app/system/page-44e7954c0e48f4ba.js +1 -0
- package/dist/web/_next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js +1 -0
- package/dist/web/_next/static/chunks/framework-9ac6a346bd42cedd.js +1 -0
- package/dist/web/_next/static/chunks/main-6be4444e2d667e41.js +1 -0
- package/dist/web/_next/static/chunks/main-app-e3a5f713ba31557e.js +1 -0
- package/dist/web/_next/static/chunks/pages/_app-32d20d5a04949d29.js +1 -0
- package/dist/web/_next/static/chunks/pages/_error-abbab53f25ea29b0.js +1 -0
- package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/web/_next/static/chunks/webpack-0f31b926d9a9c5a4.js +1 -0
- package/dist/web/_next/static/css/d77735cdc9020a86.css +1 -0
- package/dist/web/_next/static/u8rZNwh4d3n-HGJnfZ72Y/_buildManifest.js +1 -0
- package/dist/web/_next/static/u8rZNwh4d3n-HGJnfZ72Y/_ssgManifest.js +1 -0
- package/dist/web/appearance.html +15 -0
- package/dist/web/appearance.txt +26 -0
- package/dist/web/defaults.html +15 -0
- package/dist/web/defaults.txt +26 -0
- package/dist/web/history.html +15 -0
- package/dist/web/history.txt +26 -0
- package/dist/web/import.html +15 -0
- package/dist/web/import.txt +26 -0
- package/dist/web/index.html +15 -0
- package/dist/web/index.txt +26 -0
- package/dist/web/level1.html +15 -0
- package/dist/web/level1.txt +26 -0
- package/dist/web/presets.html +18 -0
- package/dist/web/presets.txt +26 -0
- package/dist/web/sessions/view.html +15 -0
- package/dist/web/sessions/view.txt +26 -0
- package/dist/web/system.html +15 -0
- package/dist/web/system.txt +26 -0
- package/package.json +111 -0
- package/prompts/level1.md +535 -0
- package/prompts/organisation.bg.md +44 -0
- package/prompts/organisation.en.md +44 -0
- package/prompts/persona.bg.md +17 -0
- package/prompts/persona.en.md +17 -0
- package/prompts/review1.md +277 -0
- package/prompts/work.bg.md +19 -0
- package/prompts/work.en.md +19 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `cop` — the command line.
|
|
4
|
+
*
|
|
5
|
+
* cop login sign in once; the Edge profile is reused afterwards
|
|
6
|
+
* cop doctor [run.yaml] check the machine before trusting a run to it
|
|
7
|
+
* cop mirror <run.yaml> refresh the Desktop folder without touching the chat
|
|
8
|
+
* cop run <run.yaml> the loop
|
|
9
|
+
* cop chat print the last run's conversation link
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { installLayout } from './config/layout.js';
|
|
16
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
17
|
+
import { createInterface } from 'node:readline/promises';
|
|
18
|
+
import { stdin, stdout } from 'node:process';
|
|
19
|
+
import { loadConfig, expandPath } from './config/schema.js';
|
|
20
|
+
import { CopilotTransport } from './transport/copilotTransport.js';
|
|
21
|
+
import { runSession } from './orchestrator/taskRunner.js';
|
|
22
|
+
import { SessionStore } from './session/store.js';
|
|
23
|
+
import { EventBus } from './session/events.js';
|
|
24
|
+
import { terminalAuthorizer, unattendedAuthorizer } from './exec/authorizer.js';
|
|
25
|
+
import { unattendedPrecondition } from './exec/policy.js';
|
|
26
|
+
import { mirrorProject, describeMirror, listSelectableDirs } from './context/projectMirror.js';
|
|
27
|
+
import { isOwnCheckout } from './exec/workDir.js';
|
|
28
|
+
import { defaultExportDir, desktopIsSynced, resolveDesktopDir } from './context/contextFiles.js';
|
|
29
|
+
import { Url } from './transport/locators.js';
|
|
30
|
+
import { findEdgeUsingProfile } from './transport/profileLock.js';
|
|
31
|
+
import { assessIsolation, readIsolationSignals } from './exec/isolation.js';
|
|
32
|
+
const program = new Command();
|
|
33
|
+
program
|
|
34
|
+
.name('cop')
|
|
35
|
+
.description('Runs a task loop with Microsoft 365 Copilot on a Windows machine.')
|
|
36
|
+
.version('0.1.0');
|
|
37
|
+
const DEFAULT_PROFILE = expandPath('~/AppData/Local/copilot-operator/edge-profile', process.cwd());
|
|
38
|
+
program
|
|
39
|
+
.command('start')
|
|
40
|
+
.description('start copilot-operator for this project: the API and the web interface, on 127.0.0.1')
|
|
41
|
+
.option('--port <port>', 'the port to listen on', '4000')
|
|
42
|
+
.option('--open', 'open the interface in the default browser once it is up')
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
process.env.COP_API_PORT = String(Number(opts.port) || 4000);
|
|
45
|
+
// The API runs in this process; importing it starts it. See src/api/main.ts.
|
|
46
|
+
await import('./api/main.js');
|
|
47
|
+
if (opts.open) {
|
|
48
|
+
const url = `http://127.0.0.1:${process.env.COP_API_PORT}/`;
|
|
49
|
+
const deadline = Date.now() + 30_000;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
const up = await fetch(`${url}api/health`).then((r) => r.ok).catch(() => false);
|
|
52
|
+
if (up)
|
|
53
|
+
break;
|
|
54
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
55
|
+
}
|
|
56
|
+
// Explorer hands an address to the default browser; no shell, no `start`.
|
|
57
|
+
spawn('explorer.exe', [url], { stdio: 'ignore', detached: true, windowsHide: true }).unref();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('logout')
|
|
62
|
+
.description("sign the bot's browser profile out of Microsoft 365; the profile itself stays")
|
|
63
|
+
.option('-p, --profile <dir>', 'profile directory', DEFAULT_PROFILE)
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
const transport = new CopilotTransport({
|
|
66
|
+
profileDir: opts.profile,
|
|
67
|
+
transportDir: join(opts.profile, '_transport'),
|
|
68
|
+
chatUrl: 'https://m365.cloud.microsoft/chat',
|
|
69
|
+
channel: 'msedge',
|
|
70
|
+
headless: false,
|
|
71
|
+
replyTimeoutMs: 60_000,
|
|
72
|
+
signInTimeoutMs: 60_000,
|
|
73
|
+
humanWaitMs: 60_000,
|
|
74
|
+
onEvent: (e) => process.stdout.write(` ${e}\n`),
|
|
75
|
+
});
|
|
76
|
+
await transport.open();
|
|
77
|
+
try {
|
|
78
|
+
await transport.signOut();
|
|
79
|
+
console.log('Signed out. The profile folder is still there; `cop login --fresh` deletes it.');
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await transport.close();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
program
|
|
86
|
+
.command('login')
|
|
87
|
+
.description('open Edge with the bot profile and wait for you to sign in')
|
|
88
|
+
.option('-p, --profile <dir>', 'profile directory', DEFAULT_PROFILE)
|
|
89
|
+
.option('--url <url>', 'chat url', Url.chat)
|
|
90
|
+
.option('-a, --account <upn>', 'the account to sign in as, e.g. 155676@365kit.org. Signs out first and asks for this one.')
|
|
91
|
+
.option('--fresh', 'delete the bot profile first, so nothing is remembered from before')
|
|
92
|
+
.action(async (opts) => {
|
|
93
|
+
if (opts.fresh) {
|
|
94
|
+
if (existsSync(opts.profile)) {
|
|
95
|
+
console.log(`This will delete the bot's browser profile:\n ${opts.profile}`);
|
|
96
|
+
console.log('It only contains the bot\'s own browser session. Your normal Edge is untouched.');
|
|
97
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
98
|
+
const answer = (await rl.question('Delete it? [y/N] ')).trim().toLowerCase();
|
|
99
|
+
rl.close();
|
|
100
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
101
|
+
console.log('Left it alone. Run without --fresh to keep the existing profile.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await rm(opts.profile, { recursive: true, force: true });
|
|
105
|
+
console.log('Profile deleted.');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log('No existing profile to delete.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const transport = new CopilotTransport({
|
|
112
|
+
profileDir: opts.profile,
|
|
113
|
+
transportDir: join(opts.profile, '_transport'),
|
|
114
|
+
chatUrl: opts.url,
|
|
115
|
+
channel: 'msedge',
|
|
116
|
+
headless: false,
|
|
117
|
+
replyTimeoutMs: 60_000,
|
|
118
|
+
signInTimeoutMs: 15 * 60_000,
|
|
119
|
+
humanWaitMs: 15 * 60_000,
|
|
120
|
+
onEvent: (e) => process.stdout.write(` ${e}\n`),
|
|
121
|
+
});
|
|
122
|
+
await transport.open();
|
|
123
|
+
try {
|
|
124
|
+
if (opts.account) {
|
|
125
|
+
// Edge on a Windows machine will sign the profile in with whatever account the OS
|
|
126
|
+
// knows, which is how the wrong user ends up in the chat without anyone choosing.
|
|
127
|
+
// Signing out first, then asking for this specific account, is what stops that.
|
|
128
|
+
console.log(`Signing out first, then asking for ${opts.account}.`);
|
|
129
|
+
await transport.signOut();
|
|
130
|
+
await transport.gotoChatAs(opts.account, opts.url);
|
|
131
|
+
}
|
|
132
|
+
console.log('Sign in to Microsoft 365 Copilot in the Edge window that just opened.');
|
|
133
|
+
if (opts.account) {
|
|
134
|
+
console.log(`Use ${opts.account}. If Edge offers a different account, choose`);
|
|
135
|
+
console.log('"Use another account" and type this one.');
|
|
136
|
+
}
|
|
137
|
+
console.log('If a human-verification box appears, complete it yourself; the bot will not.');
|
|
138
|
+
console.log('The bot never types credentials. Waiting for the chat to appear...');
|
|
139
|
+
await transport.ensureSignedIn(opts.account ? undefined : opts.url);
|
|
140
|
+
const accounts = await transport.findAccountsInPage();
|
|
141
|
+
if (opts.account) {
|
|
142
|
+
const wanted = opts.account.toLowerCase();
|
|
143
|
+
const match = accounts.some((a) => a.toLowerCase() === wanted);
|
|
144
|
+
if (match) {
|
|
145
|
+
console.log(`Signed in as ${opts.account}.`);
|
|
146
|
+
}
|
|
147
|
+
else if (accounts.length > 0) {
|
|
148
|
+
throw new Error(`Signed in, but not as ${opts.account}. The page shows: ${accounts.join(', ')}.\n` +
|
|
149
|
+
'Run "cop login --account <upn> --fresh" to wipe the profile and start clean.');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.log(`Signed in. Could not read the account from the page, so ${opts.account} is unverified.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (accounts.length > 0) {
|
|
156
|
+
console.log(`Signed in. The page shows: ${accounts.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(`Profile saved at ${opts.profile}`);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
// Always close. A left-open Edge keeps the profile locked, and the next run would
|
|
162
|
+
// then fail with a message about a closed browser that explains nothing.
|
|
163
|
+
await transport.close();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
program
|
|
167
|
+
.command('open')
|
|
168
|
+
.description("open the bot's own browser and leave it to you, for testing by hand")
|
|
169
|
+
.option('-p, --profile <dir>', 'profile directory', DEFAULT_PROFILE)
|
|
170
|
+
.option('--url <url>', 'chat url', Url.chat)
|
|
171
|
+
.action(async (opts) => {
|
|
172
|
+
const transport = new CopilotTransport({
|
|
173
|
+
profileDir: opts.profile,
|
|
174
|
+
transportDir: join(opts.profile, '_transport'),
|
|
175
|
+
chatUrl: opts.url,
|
|
176
|
+
channel: 'msedge',
|
|
177
|
+
headless: false,
|
|
178
|
+
replyTimeoutMs: 60_000,
|
|
179
|
+
signInTimeoutMs: 15 * 60_000,
|
|
180
|
+
humanWaitMs: 15 * 60_000,
|
|
181
|
+
onEvent: (e, d) => process.stdout.write(` ${e}${d && Object.keys(d).length ? ' ' + JSON.stringify(d) : ''}
|
|
182
|
+
`),
|
|
183
|
+
});
|
|
184
|
+
await transport.open();
|
|
185
|
+
try {
|
|
186
|
+
await transport.ensureSignedIn();
|
|
187
|
+
const where = await transport.surface();
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log("This is the bot's own browser. Nothing is automated from here.");
|
|
190
|
+
console.log(` profile : ${opts.profile}`);
|
|
191
|
+
console.log(` surface : ${where === 'work' ? 'Microsoft 365 Copilot (work)' : where}`);
|
|
192
|
+
console.log(' windows : this profile has no extensions, so if you can see a Grammarly');
|
|
193
|
+
console.log(' or similar icon, you are looking at a different Edge window.');
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log('Type a long message here by hand and send it. That tells us whether the');
|
|
196
|
+
console.log('chat treats the bot differently from you, in the same browser.');
|
|
197
|
+
console.log('');
|
|
198
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
199
|
+
await rl.question('Press Enter here when you are done, to close the browser... ');
|
|
200
|
+
rl.close();
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await transport.close();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
program
|
|
207
|
+
.command('doctor')
|
|
208
|
+
.description('check that this machine can run the bot')
|
|
209
|
+
.argument('[config]', 'path to run.yaml')
|
|
210
|
+
.action(async (configPath) => {
|
|
211
|
+
const problems = [];
|
|
212
|
+
const say = (ok, text) => {
|
|
213
|
+
console.log(` ${ok ? 'ok ' : 'FAIL'} ${text}`);
|
|
214
|
+
if (!ok)
|
|
215
|
+
problems.push(text);
|
|
216
|
+
};
|
|
217
|
+
const node = process.versions.node;
|
|
218
|
+
say(Number(node.split('.')[0]) >= 20, `Node.js ${node} (20 or newer required)`);
|
|
219
|
+
const edge = [
|
|
220
|
+
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
|
221
|
+
'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
|
|
222
|
+
].find((p) => existsSync(p));
|
|
223
|
+
say(Boolean(edge), edge ? `Microsoft Edge at ${edge}` : 'Microsoft Edge not found');
|
|
224
|
+
const pwsh = spawnSync('pwsh', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()'], {
|
|
225
|
+
encoding: 'utf8',
|
|
226
|
+
});
|
|
227
|
+
const winps = spawnSync('powershell', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()'], {
|
|
228
|
+
encoding: 'utf8',
|
|
229
|
+
});
|
|
230
|
+
say(pwsh.status === 0 || winps.status === 0, pwsh.status === 0
|
|
231
|
+
? `PowerShell 7 (${pwsh.stdout.trim()})`
|
|
232
|
+
: winps.status === 0
|
|
233
|
+
? `Windows PowerShell (${winps.stdout.trim()}); pwsh not found`
|
|
234
|
+
: 'no PowerShell found');
|
|
235
|
+
/*
|
|
236
|
+
* The three things a machine that is not this one has failed on, or would.
|
|
237
|
+
*
|
|
238
|
+
* git: version control runs `git` from PATH for every task; a laptop with git only inside
|
|
239
|
+
* an IDE has none there. npm: every plan's first task is `npm install`, and a corporate proxy
|
|
240
|
+
* that is not configured for npm fails it with a message the chat model then spends an
|
|
241
|
+
* iteration reading. Models: the picker's names belong to the tenant, so the defaults saved
|
|
242
|
+
* here ("GPT 5.6 Think deeper") may not exist on another account — the run then goes on with
|
|
243
|
+
* whatever the chat is set to, and says so in a warning nobody reads until the review turns
|
|
244
|
+
* out to have run on the same model as the work.
|
|
245
|
+
*/
|
|
246
|
+
const git = spawnSync('git', ['--version'], { encoding: 'utf8' });
|
|
247
|
+
say(git.status === 0, git.status === 0 ? git.stdout.trim() : 'git not found on PATH — version control cannot branch or commit');
|
|
248
|
+
// One command string through the shell: on Windows npm is npm.cmd, which Node will not
|
|
249
|
+
// spawn without a shell, and a shell with an args array is what Node deprecates.
|
|
250
|
+
const registry = spawnSync('npm config get registry', { encoding: 'utf8', shell: true }).stdout?.trim() || '(unknown registry)';
|
|
251
|
+
const ping = spawnSync('npm ping --fetch-timeout=15000 --fetch-retries=0', { encoding: 'utf8', timeout: 25_000, shell: true });
|
|
252
|
+
say(ping.status === 0, ping.status === 0
|
|
253
|
+
? `npm reaches ${registry}`
|
|
254
|
+
: `npm cannot reach ${registry} — behind a proxy, set it for npm (npm config set proxy / https-proxy) or HTTPS_PROXY; every plan starts with npm install`);
|
|
255
|
+
const dataDir = installLayout().dataDir;
|
|
256
|
+
const readJson = (file) => {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(readFileSync(join(dataDir, file), 'utf8'));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const settings = readJson('settings.json');
|
|
265
|
+
/*
|
|
266
|
+
* Where the bot is running, which is the only thing that actually contains a command once it
|
|
267
|
+
* runs. Reported here rather than left to the README, because a machine that ignored the
|
|
268
|
+
* recommendation looked exactly like one that had followed it. The claim is the operator's;
|
|
269
|
+
* `doctor` only says what it can see and where the two disagree. See `exec/isolation.ts`.
|
|
270
|
+
*/
|
|
271
|
+
const posture = assessIsolation(settings?.execution?.isolation ?? 'none', readIsolationSignals());
|
|
272
|
+
say(posture.warnings.length === 0, `isolation: ${posture.claim}, running as ${posture.signals.user}${posture.signals.elevated === true ? ' (ELEVATED)' : ''}`);
|
|
273
|
+
for (const concern of posture.warnings)
|
|
274
|
+
say(false, ` ${concern}`);
|
|
275
|
+
const models = readJson('models.json');
|
|
276
|
+
const copilot = settings?.copilot ?? {};
|
|
277
|
+
const wanted = [
|
|
278
|
+
['default model', (copilot.defaultModel ?? '').trim()],
|
|
279
|
+
['default review model', (copilot.defaultReviewModel ?? '').trim()],
|
|
280
|
+
].filter(([, name]) => name !== '');
|
|
281
|
+
if (wanted.length === 0) {
|
|
282
|
+
console.log(' note no default model is set; sessions leave the chat on whatever it shows');
|
|
283
|
+
}
|
|
284
|
+
else if (!models?.options?.length) {
|
|
285
|
+
console.log(` note ${wanted.map(([w, n]) => `${w} "${n}"`).join(', ')} set, but the picker has never been read here — read it from the Settings page before trusting them`);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const names = new Set(models.options.map((o) => o.name));
|
|
289
|
+
for (const [what, name] of wanted) {
|
|
290
|
+
say(names.has(name), names.has(name) ? `${what} "${name}" is in the picker read on this machine` : `${what} "${name}" is not in the picker read on this machine (${models.options.length} option(s)) — the run would fall back to whatever the chat shows`);
|
|
291
|
+
}
|
|
292
|
+
if (wanted.length === 2 && wanted[0][1] === wanted[1][1]) {
|
|
293
|
+
console.log(' note the review runs on the same model as the work; a different one catches more');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const edgeUsers = findEdgeUsingProfile(DEFAULT_PROFILE);
|
|
297
|
+
if (Array.isArray(edgeUsers) && edgeUsers.length > 0) {
|
|
298
|
+
const pids = edgeUsers.map((u) => u.pid);
|
|
299
|
+
say(false, `Edge is already running with the bot profile (${pids.length} process(es)). A run would ` +
|
|
300
|
+
`fail with "Target page, context or browser has been closed". Close that Edge window, or:`);
|
|
301
|
+
console.log(` Stop-Process -Id ${pids.join(',')} -Force`);
|
|
302
|
+
}
|
|
303
|
+
else if (edgeUsers === 'unknown') {
|
|
304
|
+
console.log(' note could not check whether Edge is holding the bot profile');
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.log(' ok no Edge process is holding the bot profile');
|
|
308
|
+
}
|
|
309
|
+
const desktop = resolveDesktopDir();
|
|
310
|
+
say(existsSync(desktop), `Desktop at ${desktop}`);
|
|
311
|
+
console.log(` ${desktopIsSynced() ? 'ok ' : 'note'} Desktop ${desktopIsSynced() ? 'is' : 'is NOT'} backed up by OneDrive` +
|
|
312
|
+
`${desktopIsSynced() ? '' : ' — the mirror folder will stay on this machine only'}`);
|
|
313
|
+
if (configPath) {
|
|
314
|
+
try {
|
|
315
|
+
const cfg = await loadConfig(configPath);
|
|
316
|
+
say(true, `config ${cfg.configPath} parsed`);
|
|
317
|
+
// The configured cwd is only the fallback — a session's commands run in its project —
|
|
318
|
+
// and this checkout is refused as a fallback, so say so here rather than at run time.
|
|
319
|
+
const ownCwd = isOwnCheckout(cfg.resolved.cwd);
|
|
320
|
+
say(existsSync(cfg.resolved.cwd) && !ownCwd, `fallback working directory ${cfg.resolved.cwd}` +
|
|
321
|
+
(ownCwd ? " — this is the runner's own checkout; a session with no project folder will refuse to run" : ''));
|
|
322
|
+
const profileExists = existsSync(cfg.resolved.profileDir);
|
|
323
|
+
console.log(` ${profileExists ? 'ok ' : 'note'} browser profile ${cfg.resolved.profileDir}` +
|
|
324
|
+
`${profileExists ? '' : ' — run "cop login" first'}`);
|
|
325
|
+
if (cfg.projectMirror.enabled) {
|
|
326
|
+
say(existsSync(cfg.resolved.mirrorRootDir), `project root ${cfg.resolved.mirrorRootDir}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
say(false, e.message);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
console.log('');
|
|
334
|
+
console.log(problems.length === 0 ? 'Ready.' : `${problems.length} problem(s) to fix first.`);
|
|
335
|
+
process.exitCode = problems.length === 0 ? 0 : 1;
|
|
336
|
+
});
|
|
337
|
+
program
|
|
338
|
+
.command('dirs')
|
|
339
|
+
.description('list the directories of a project that can be selected for the mirror')
|
|
340
|
+
.argument('<projectRoot>')
|
|
341
|
+
.action(async (root) => {
|
|
342
|
+
const dirs = await listSelectableDirs(root);
|
|
343
|
+
if (dirs.length === 0)
|
|
344
|
+
console.log('(no selectable directories found)');
|
|
345
|
+
for (const d of dirs)
|
|
346
|
+
console.log(d);
|
|
347
|
+
});
|
|
348
|
+
program
|
|
349
|
+
.command('models')
|
|
350
|
+
.description('read the chat\'s model picker and print what it offers right now')
|
|
351
|
+
.option('-p, --profile <dir>', 'profile directory', DEFAULT_PROFILE)
|
|
352
|
+
.option('--url <url>', 'chat url', Url.chat)
|
|
353
|
+
.option('--json', 'print the raw list, for pasting into an issue')
|
|
354
|
+
.option('--set <name>', 'also switch the chat to this model, by its exact name')
|
|
355
|
+
.action(async (opts) => {
|
|
356
|
+
const transport = new CopilotTransport({
|
|
357
|
+
profileDir: opts.profile,
|
|
358
|
+
transportDir: join(installLayout().runsDir, '_models'),
|
|
359
|
+
chatUrl: opts.url,
|
|
360
|
+
channel: 'msedge',
|
|
361
|
+
headless: false,
|
|
362
|
+
replyTimeoutMs: 60_000,
|
|
363
|
+
signInTimeoutMs: 900_000,
|
|
364
|
+
onEvent: (e, d) => console.log(` [${e}]${d ? ' ' + JSON.stringify(d) : ''}`),
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
await transport.open();
|
|
368
|
+
await transport.ensureSignedIn();
|
|
369
|
+
const { options, current, note } = await transport.listModels();
|
|
370
|
+
if (opts.json) {
|
|
371
|
+
console.log(JSON.stringify({ current, note, options }, null, 2));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
console.log(`\ncurrently on: ${current ?? '(the picker did not say)'}`);
|
|
375
|
+
if (note)
|
|
376
|
+
console.log(note);
|
|
377
|
+
if (options.length === 0)
|
|
378
|
+
console.log('(no options could be read from the picker)');
|
|
379
|
+
for (const o of options) {
|
|
380
|
+
const marks = [o.selected ? 'selected' : '', o.disabled ? 'unavailable' : ''].filter(Boolean).join(', ');
|
|
381
|
+
const where = o.group ? `${o.group} > ` : '';
|
|
382
|
+
console.log(` - ${where}${o.name}${marks ? ` [${marks}]` : ''}`);
|
|
383
|
+
const extra = o.raw.split('\n').slice(1).join(' ').trim();
|
|
384
|
+
if (extra)
|
|
385
|
+
console.log(` ${extra.slice(0, 120)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (opts.set) {
|
|
389
|
+
const result = await transport.selectModel(opts.set);
|
|
390
|
+
console.log(result.ok ? `switched to: ${result.current}` : `not switched: ${result.reason}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
await transport.close();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
program
|
|
398
|
+
.command('mirror')
|
|
399
|
+
.description('refresh the Desktop folder from the project, without touching the chat')
|
|
400
|
+
.argument('<config>', 'path to run.yaml')
|
|
401
|
+
.action(async (configPath) => {
|
|
402
|
+
const cfg = await loadConfig(configPath);
|
|
403
|
+
if (!cfg.projectMirror.enabled) {
|
|
404
|
+
console.log('projectMirror.enabled is false in this config; nothing to do.');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const targetDir = cfg.resolved.mirrorTargetDir ?? defaultExportDir();
|
|
408
|
+
await mkdir(targetDir, { recursive: true });
|
|
409
|
+
const result = await mirrorProject({
|
|
410
|
+
rootDir: cfg.resolved.mirrorRootDir,
|
|
411
|
+
includeDirs: cfg.projectMirror.includeDirs,
|
|
412
|
+
excludeDirs: cfg.projectMirror.excludeDirs,
|
|
413
|
+
targetDir,
|
|
414
|
+
separator: cfg.projectMirror.separator,
|
|
415
|
+
txtMode: cfg.projectMirror.txtMode,
|
|
416
|
+
respectGitignore: cfg.projectMirror.respectGitignore,
|
|
417
|
+
ignoreDirs: cfg.projectMirror.ignoreDirs,
|
|
418
|
+
includeEnvFiles: cfg.projectMirror.includeEnvFiles,
|
|
419
|
+
maxFileBytes: cfg.projectMirror.maxFileBytes,
|
|
420
|
+
});
|
|
421
|
+
console.log(`${targetDir}`);
|
|
422
|
+
console.log(describeMirror(result));
|
|
423
|
+
for (const s of result.skipped)
|
|
424
|
+
console.log(` skipped ${s.relPath}: ${s.reason}`);
|
|
425
|
+
if (!desktopIsSynced()) {
|
|
426
|
+
console.log('note: the Desktop is not backed up by OneDrive, so these copies stay local.');
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
program
|
|
430
|
+
.command('run')
|
|
431
|
+
.description('run one task from a run.yaml: creates a session, runs it, prints the summary')
|
|
432
|
+
.argument('<config>', 'path to run.yaml')
|
|
433
|
+
.option('--unattended', 'do not ask before each step (dangerous)')
|
|
434
|
+
.action(async (configPath, opts) => {
|
|
435
|
+
const cfg = await loadConfig(configPath);
|
|
436
|
+
if (opts.unattended)
|
|
437
|
+
cfg.execution.mode = 'unattended';
|
|
438
|
+
if (!cfg.resolved.taskText.trim()) {
|
|
439
|
+
throw new Error(`${cfg.configPath} has no "task". Add one, inline or as { file: ... }.`);
|
|
440
|
+
}
|
|
441
|
+
if (cfg.execution.mode === 'unattended') {
|
|
442
|
+
/*
|
|
443
|
+
* Refused here rather than a hundred lines later as a run whose every step comes back
|
|
444
|
+
* refused. The step gate holds the same rule; this is so the answer arrives before a browser
|
|
445
|
+
* is opened, and names the setting to change. See `unattendedPrecondition`.
|
|
446
|
+
*/
|
|
447
|
+
const blocked = unattendedPrecondition({
|
|
448
|
+
mode: 'unattended',
|
|
449
|
+
allowedPrograms: cfg.execution.allowedPrograms,
|
|
450
|
+
isolation: cfg.execution.isolation,
|
|
451
|
+
lockedToConfirm: cfg.policyLock?.maxMode === 'confirm',
|
|
452
|
+
});
|
|
453
|
+
if (blocked)
|
|
454
|
+
throw new Error(`${blocked}\n\nOr drop --unattended and approve the steps as they come.`);
|
|
455
|
+
console.log('UNATTENDED: commands written by Copilot will run without asking.');
|
|
456
|
+
}
|
|
457
|
+
const store = new SessionStore(cfg.resolved.dataDir, cfg.resolved.level1Path);
|
|
458
|
+
await store.init();
|
|
459
|
+
const session = await store.createSession(cfg.copilot.label, {
|
|
460
|
+
enabled: cfg.projectMirror.enabled,
|
|
461
|
+
rootDir: cfg.resolved.mirrorRootDir ?? '',
|
|
462
|
+
includeDirs: cfg.projectMirror.includeDirs,
|
|
463
|
+
excludeDirs: cfg.projectMirror.excludeDirs,
|
|
464
|
+
respectGitignore: cfg.projectMirror.respectGitignore,
|
|
465
|
+
includeEnvFiles: cfg.projectMirror.includeEnvFiles,
|
|
466
|
+
});
|
|
467
|
+
await store.updateSession(session.id, (s) => {
|
|
468
|
+
s.onFailure = cfg.execution.continueOnFailure ? 'continue' : 'stop';
|
|
469
|
+
});
|
|
470
|
+
const task = await store.addTask(session.id, {
|
|
471
|
+
title: cfg.copilot.label,
|
|
472
|
+
level2: cfg.resolved.level2Text,
|
|
473
|
+
prompt: cfg.resolved.taskText,
|
|
474
|
+
});
|
|
475
|
+
const bus = new EventBus();
|
|
476
|
+
// Task-level events are already printed by the run log; session-level ones are not.
|
|
477
|
+
bus.subscribe(session.id, (e) => {
|
|
478
|
+
if (!e.taskId && e.message)
|
|
479
|
+
console.log(` ${e.message}`);
|
|
480
|
+
});
|
|
481
|
+
const policy = {
|
|
482
|
+
mode: cfg.execution.mode,
|
|
483
|
+
denyPatterns: cfg.execution.denyPatterns,
|
|
484
|
+
allowedPrograms: cfg.execution.allowedPrograms,
|
|
485
|
+
isolation: cfg.execution.isolation,
|
|
486
|
+
lockedToConfirm: cfg.policyLock?.maxMode === 'confirm',
|
|
487
|
+
};
|
|
488
|
+
const authorizer = cfg.execution.mode === 'unattended'
|
|
489
|
+
? unattendedAuthorizer(policy)
|
|
490
|
+
: terminalAuthorizer(policy, (line) => console.log(line));
|
|
491
|
+
await runSession(session.id, { cfg, store, bus, authorizer });
|
|
492
|
+
const final = await store.getSession(session.id);
|
|
493
|
+
const done = final?.tasks.find((t) => t.id === task.id);
|
|
494
|
+
console.log('');
|
|
495
|
+
console.log(`task "${done?.title}": ${done?.status} after ${done?.iterations ?? 0} iteration(s)`);
|
|
496
|
+
if (done?.summary)
|
|
497
|
+
console.log(`
|
|
498
|
+
Summary:
|
|
499
|
+
${done.summary}`);
|
|
500
|
+
if (done?.reason)
|
|
501
|
+
console.log(`
|
|
502
|
+
Reason: ${done.reason}`);
|
|
503
|
+
if (final?.chat)
|
|
504
|
+
console.log(`
|
|
505
|
+
chat: ${final.chat.name} — ${final.chat.url}`);
|
|
506
|
+
if (done?.runId)
|
|
507
|
+
console.log(`log: ${join(cfg.resolved.runsDir, done.runId, 'task-log.txt')}`);
|
|
508
|
+
process.exitCode = done?.status === 'done' ? 0 : 1;
|
|
509
|
+
});
|
|
510
|
+
program
|
|
511
|
+
.command('chat')
|
|
512
|
+
.description("print the most recent session's conversation")
|
|
513
|
+
.argument('[config]', 'path to run.yaml', 'run.yaml')
|
|
514
|
+
.action(async (configPath) => {
|
|
515
|
+
const cfg = await loadConfig(configPath);
|
|
516
|
+
const store = new SessionStore(cfg.resolved.dataDir, cfg.resolved.level1Path);
|
|
517
|
+
const sessions = await store.listSessions();
|
|
518
|
+
const withChat = sessions.find((s) => s.chat);
|
|
519
|
+
if (!withChat?.chat) {
|
|
520
|
+
console.log('No session has a conversation yet.');
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const c = withChat.chat;
|
|
524
|
+
console.log(`${c.name}
|
|
525
|
+
${c.url}
|
|
526
|
+
session ${withChat.id}, ${withChat.tasks.length} task(s), created ${c.createdAt}`);
|
|
527
|
+
});
|
|
528
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
529
|
+
console.error(e.message);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where everything is, for the two ways this program is installed.
|
|
3
|
+
*
|
|
4
|
+
* checkout a git clone of the repository, started with `npm start`. The records live beside
|
|
5
|
+
* the code — `data/` and `runs/` in the clone — as they always have, and the UI is a
|
|
6
|
+
* Next.js dev server of its own.
|
|
7
|
+
* package `npm i -D copilot-operator` in a project, started with `npx cop start`. The code
|
|
8
|
+
* is in `node_modules`, which `npm install` rewrites whenever it likes, so nothing of
|
|
9
|
+
* the operator's may live there: the records go to `<project>/.copilot-operator/`,
|
|
10
|
+
* and the UI is the prebuilt one shipped in the package, served by the API itself.
|
|
11
|
+
*
|
|
12
|
+
* Which one is decided by where the code is: inside a `node_modules` folder is a package. The
|
|
13
|
+
* project a package belongs to is the folder that holds that `node_modules` — the one whose
|
|
14
|
+
* `package.json` named it as a dependency — unless `COP_PROJECT_ROOT` says otherwise.
|
|
15
|
+
*
|
|
16
|
+
* Everything that used to reach for `process.cwd()` or the clone's root to find a prompt, the
|
|
17
|
+
* data folder or the runs folder asks here instead, so the two installs cannot disagree about
|
|
18
|
+
* where a thing is.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
22
|
+
import { botRootDir } from '../exec/workDir.js';
|
|
23
|
+
/** The name of the records folder a package keeps inside the project. */
|
|
24
|
+
export const HOME_FOLDER = '.copilot-operator';
|
|
25
|
+
/** The project a package install belongs to: the folder holding the `node_modules` the code sits in. */
|
|
26
|
+
function projectOfPackage(botRoot) {
|
|
27
|
+
const parent = dirname(resolve(botRoot));
|
|
28
|
+
// `<project>/node_modules/copilot-operator`
|
|
29
|
+
if (basename(parent).toLowerCase() === 'node_modules')
|
|
30
|
+
return dirname(parent);
|
|
31
|
+
// `<project>/node_modules/@scope/copilot-operator`
|
|
32
|
+
if (basename(parent).startsWith('@') && basename(dirname(parent)).toLowerCase() === 'node_modules')
|
|
33
|
+
return dirname(dirname(parent));
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
export function installLayout(env = process.env, cwd = process.cwd(), botRoot = botRootDir()) {
|
|
37
|
+
const promptsDir = join(botRoot, 'prompts');
|
|
38
|
+
const shippedWeb = join(botRoot, 'dist', 'web');
|
|
39
|
+
const webDir = existsSync(join(shippedWeb, 'index.html')) ? shippedWeb : null;
|
|
40
|
+
const packaged = projectOfPackage(botRoot);
|
|
41
|
+
if (packaged) {
|
|
42
|
+
const projectRoot = resolve(env.COP_PROJECT_ROOT ?? packaged);
|
|
43
|
+
const homeDir = join(projectRoot, HOME_FOLDER);
|
|
44
|
+
return {
|
|
45
|
+
mode: 'package',
|
|
46
|
+
botRoot,
|
|
47
|
+
promptsDir,
|
|
48
|
+
projectRoot,
|
|
49
|
+
homeDir,
|
|
50
|
+
dataDir: resolve(env.COP_DATA_DIR ?? join(homeDir, 'data')),
|
|
51
|
+
runsDir: join(homeDir, 'runs'),
|
|
52
|
+
webDir,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const projectRoot = resolve(env.COP_PROJECT_ROOT ?? cwd);
|
|
56
|
+
return {
|
|
57
|
+
mode: 'checkout',
|
|
58
|
+
botRoot,
|
|
59
|
+
promptsDir,
|
|
60
|
+
projectRoot,
|
|
61
|
+
homeDir: projectRoot,
|
|
62
|
+
dataDir: resolve(env.COP_DATA_DIR ?? join(projectRoot, 'data')),
|
|
63
|
+
runsDir: join(projectRoot, 'runs'),
|
|
64
|
+
webDir,
|
|
65
|
+
};
|
|
66
|
+
}
|