atris 3.80.0 → 3.82.1
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 +10 -0
- package/atris/atris.md +14 -0
- package/atris/features/README.md +15 -0
- package/atris/policies/README.md +24 -0
- package/atris/skills/README.md +61 -45
- package/atris/skills/design/SKILL.md +6 -5
- package/atris/skills/taste/SKILL.md +39 -0
- package/atris/skills/taste/notes.md +29 -0
- package/atris/skills/taste/principles.md +23 -0
- package/atris/skills/wiki/SKILL.md +14 -2
- package/atris/skills/wiki/index-lines.md +30 -0
- package/atris/skills/wiki/measure.md +20 -0
- package/atris/skills/wiki/note-shape.md +16 -0
- package/atris/skills/wiki/upkeep.md +16 -0
- package/atris/team/README.md +38 -0
- package/atris.md +14 -0
- package/bin/atris.js +1 -1
- package/commands/activate.js +1 -1
- package/commands/auth.js +2 -2
- package/commands/autoland.js +25 -26
- package/commands/autopilot-front.js +46 -1
- package/commands/autopilot.js +55 -25
- package/commands/clean.js +22 -11
- package/commands/computer.js +166 -2413
- package/commands/doc-health.js +48 -1
- package/commands/improve.js +16 -3
- package/commands/init.js +4 -0
- package/commands/integrations.js +69 -1616
- package/commands/mission.js +109 -13
- package/commands/release.js +41 -10
- package/commands/security-review.js +5 -1
- package/commands/site-deploy.js +5 -1
- package/commands/status.js +1 -1
- package/commands/sync.js +12 -8
- package/commands/task.js +144 -118
- package/commands/unknowns.js +9 -5
- package/commands/visualize.js +1 -12
- package/commands/workflow.js +4 -3
- package/commands/worktree.js +68 -1602
- package/commands/xp.js +23 -2113
- package/commands/youtube.js +192 -3110
- package/lib/business-core.js +3 -4
- package/lib/campaign-run-lock.js +12 -1
- package/lib/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -1
- package/lib/engine-receipt-sweep.js +64 -0
- package/lib/file-lock.js +175 -0
- package/lib/fleet.js +1 -1
- package/lib/integrations-core.js +1695 -0
- package/lib/json-file.js +22 -1
- package/lib/loop-doctor.js +36 -2
- package/lib/map-refs.js +68 -8
- package/lib/member-engine.js +1 -1
- package/lib/member-map-fold.js +49 -11
- package/lib/member-map-inbox.js +2 -1
- package/lib/mission-core.js +9 -2
- package/lib/recall-exam.js +171 -0
- package/lib/roster-markdown.js +2 -2
- package/lib/runner-command.js +5 -5
- package/lib/security-audit-classes.js +20 -2
- package/lib/security-audit.js +31 -1
- package/lib/task-db.js +105 -37
- package/lib/taste-notes.js +35 -0
- package/lib/team-lineup.js +3 -3
- package/lib/todo.js +2 -0
- package/lib/wiki.js +11 -3
- package/lib/worktree-core.js +1517 -0
- package/lib/xp-core.js +2172 -0
- package/lib/youtube-core.js +3178 -0
- package/lib/zen-apply.js +3 -1
- package/package.json +10 -3
- package/utils/auth.js +18 -4
|
@@ -0,0 +1,2447 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const { decodeJwtClaims, promptUser, abortOnAuthFailure } = require('../utils/auth');
|
|
9
|
+
const { resolveBackendRoot } = require('../utils/backend-root');
|
|
10
|
+
const api = require('../utils/api');
|
|
11
|
+
const { businessWorkspaceBase, loadBusinesses, saveBusinesses } = require('../commands/business');
|
|
12
|
+
const { consoleCommand } = require('../commands/console');
|
|
13
|
+
const { streamSession } = require('../commands/serve');
|
|
14
|
+
const { buildRemoteAtrisBootstrapCommand } = require('./runtime-bootstrap');
|
|
15
|
+
const { envForcesNonInteractive } = require('./noninteractive');
|
|
16
|
+
|
|
17
|
+
function apiRequestJson(...args) {
|
|
18
|
+
return api.apiRequestJson(...args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function askComputerSetup(question, deps = {}) {
|
|
26
|
+
const prompt = deps.prompt || promptUser;
|
|
27
|
+
return String(await prompt(question) || '').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setupComputerChoices(data) {
|
|
31
|
+
const businesses = Array.isArray(data)
|
|
32
|
+
? data
|
|
33
|
+
: (Array.isArray(data?.businesses) ? data.businesses : []);
|
|
34
|
+
return businesses
|
|
35
|
+
.filter((business) => business && business.id)
|
|
36
|
+
.map((business) => {
|
|
37
|
+
const owner = business.name || business.slug || 'business';
|
|
38
|
+
const computer = business.computer_name || business.workspace_name || '';
|
|
39
|
+
return {
|
|
40
|
+
label: computer ? `${owner}: ${computer}` : `${owner} computer`,
|
|
41
|
+
target: { type: 'business', id: business.id },
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function chooseSetupComputer(computers, deps = {}) {
|
|
47
|
+
const choices = computers.length
|
|
48
|
+
? computers
|
|
49
|
+
: [{ label: 'your personal computer', target: { type: 'user' } }];
|
|
50
|
+
if (choices.length === 1) {
|
|
51
|
+
console.log(`using ${choices[0].label}.`);
|
|
52
|
+
return choices[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
choices.forEach((computer, index) => console.log(`${index + 1}. ${computer.label}`));
|
|
56
|
+
const answer = await askComputerSetup('which computer? [1] ', deps);
|
|
57
|
+
const selected = Number.parseInt(answer || '1', 10);
|
|
58
|
+
return choices[selected >= 1 && selected <= choices.length ? selected - 1 : 0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function chooseSetupProviders(deps = {}) {
|
|
62
|
+
while (true) {
|
|
63
|
+
const answer = (await askComputerSetup(
|
|
64
|
+
'which coding accounts do you want to connect? (codex / claude / both) [both] ',
|
|
65
|
+
deps
|
|
66
|
+
) || 'both').toLowerCase();
|
|
67
|
+
if (answer === 'codex') return ['codex'];
|
|
68
|
+
if (answer === 'claude') return ['claude'];
|
|
69
|
+
if (answer === 'both') return ['codex', 'claude'];
|
|
70
|
+
console.error('choose codex, claude, or both.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const VALID_CLOUD_WORKERS = new Set(['claude', 'openai']);
|
|
75
|
+
const LOCAL_BRIDGE_RECONNECT_MS = 2000;
|
|
76
|
+
const VALID_COMPUTER_TYPES = new Set([
|
|
77
|
+
'general',
|
|
78
|
+
'business_ops',
|
|
79
|
+
'codeops',
|
|
80
|
+
'research',
|
|
81
|
+
'crm',
|
|
82
|
+
'reporting',
|
|
83
|
+
'recruiting',
|
|
84
|
+
'event_ops',
|
|
85
|
+
'support',
|
|
86
|
+
]);
|
|
87
|
+
const RECRUITING_BUSINESS_SLUG = 'atris-labs';
|
|
88
|
+
const RECRUITING_LOCAL_SYNC_COMMANDS = new Set(['pull', 'push', 'publish', 'watch', 'review', 'doctor']);
|
|
89
|
+
const KNOWN_CHAT_COMMANDS = new Set([
|
|
90
|
+
'/audit',
|
|
91
|
+
'/exit',
|
|
92
|
+
'/files',
|
|
93
|
+
'/help',
|
|
94
|
+
'/login',
|
|
95
|
+
'/model',
|
|
96
|
+
'/pwd',
|
|
97
|
+
'/quit',
|
|
98
|
+
'/reset',
|
|
99
|
+
'/run',
|
|
100
|
+
'/start',
|
|
101
|
+
'/status',
|
|
102
|
+
'/worker',
|
|
103
|
+
'/workflow',
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const CODEOPS_WORKFLOW_PROMPT = `
|
|
107
|
+
## Atris CodeOps Workflow
|
|
108
|
+
|
|
109
|
+
You are running inside Atris CodeOps with full computer permissions (permission_mode=bypassPermissions).
|
|
110
|
+
Use those permissions to inspect, edit, test, commit, push, and open PRs when the task calls for it.
|
|
111
|
+
|
|
112
|
+
Do not behave like an open-ended chat.
|
|
113
|
+
Every coding or repo operation must follow the scientific workflow:
|
|
114
|
+
OBSERVE -> HYPOTHESIS -> PLAN -> ACTION -> VALIDATION -> EVIDENCE -> NEXT STATE.
|
|
115
|
+
|
|
116
|
+
For a new coding request, first show a concise PLAN with Files, Checks, Risk, and Merge policy.
|
|
117
|
+
If the user has not clearly approved execution, ask for approval before editing.
|
|
118
|
+
If the user explicitly says to execute, proceed after the concise plan.
|
|
119
|
+
|
|
120
|
+
After work, always report:
|
|
121
|
+
- edited_files
|
|
122
|
+
- commands_run
|
|
123
|
+
- validation_result
|
|
124
|
+
- evidence
|
|
125
|
+
- pr_url if any
|
|
126
|
+
- pr_state
|
|
127
|
+
- merge_state
|
|
128
|
+
- next_task
|
|
129
|
+
|
|
130
|
+
Use one of these next states:
|
|
131
|
+
planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.
|
|
132
|
+
|
|
133
|
+
Never hide failures.
|
|
134
|
+
A blocked check or missing permission is evidence, not success.
|
|
135
|
+
`.trim();
|
|
136
|
+
|
|
137
|
+
const RECRUITING_WORKFLOW_PROMPT = `
|
|
138
|
+
## Atris Recruiting Workflow
|
|
139
|
+
|
|
140
|
+
You are running inside the recruiting computer.
|
|
141
|
+
Optimize for recruiter throughput: pipeline clarity, candidate follow-up, role context, interview loops, and decision notes.
|
|
142
|
+
Do not send outreach, DMs, emails, or calendar invites without explicit operator approval.
|
|
143
|
+
|
|
144
|
+
For each work block, report:
|
|
145
|
+
- role or pipeline touched
|
|
146
|
+
- candidates or sources reviewed
|
|
147
|
+
- artifact changed
|
|
148
|
+
- follow-up owner
|
|
149
|
+
- sync/proof status
|
|
150
|
+
`.trim();
|
|
151
|
+
|
|
152
|
+
function color(code, value) {
|
|
153
|
+
if (process.env.NO_COLOR || !process.stdout.isTTY) return String(value);
|
|
154
|
+
return `\x1b[${code}m${value}\x1b[0m`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ui = {
|
|
158
|
+
bold: (value) => color(1, value),
|
|
159
|
+
dim: (value) => color(2, value),
|
|
160
|
+
green: (value) => color(32, value),
|
|
161
|
+
yellow: (value) => color(33, value),
|
|
162
|
+
cyan: (value) => color(36, value),
|
|
163
|
+
red: (value) => color(31, value),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function useInteractiveCloudUi() {
|
|
167
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY && !envForcesNonInteractive());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function useInteractiveTerminalUi() {
|
|
171
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY && !envForcesNonInteractive());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function readPipedStdin() {
|
|
175
|
+
if (process.stdin.isTTY) return null;
|
|
176
|
+
let input = '';
|
|
177
|
+
for await (const chunk of process.stdin) {
|
|
178
|
+
input += chunk.toString();
|
|
179
|
+
}
|
|
180
|
+
return input;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function printCloudWordmark() {
|
|
184
|
+
if (!process.stdout.isTTY) return;
|
|
185
|
+
console.log(ui.cyan(' ___ __________ ________ CLOUD'));
|
|
186
|
+
console.log(ui.cyan(' / _ |/_ __/ __ \\/ _/ __/'));
|
|
187
|
+
console.log(ui.cyan(' / __ | / / / /_/ // /_\\ \\ '));
|
|
188
|
+
console.log(ui.cyan(' /_/ |_|/_/ \\____/___/___/ '));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printLocalWordmark() {
|
|
192
|
+
if (!process.stdout.isTTY) return;
|
|
193
|
+
console.log(ui.green(' ___ __________ ________ LOCAL'));
|
|
194
|
+
console.log(ui.green(' / _ |/_ __/ __ \\/ _/ __/'));
|
|
195
|
+
console.log(ui.green(' / __ | / / / /_/ // /_\\ \\ '));
|
|
196
|
+
console.log(ui.green(' /_/ |_|/_/ \\____/___/___/ '));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function activeWorker(worker) {
|
|
200
|
+
return (worker || 'claude').toLowerCase() === 'default' ? 'claude' : (worker || 'claude').toLowerCase();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatWorkerName(worker) {
|
|
204
|
+
const active = activeWorker(worker);
|
|
205
|
+
return active === 'openai' ? 'OpenAI' : 'Claude';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractAttachedWorkspaceMismatch(...values) {
|
|
209
|
+
const text = values
|
|
210
|
+
.filter((value) => value !== null && value !== undefined)
|
|
211
|
+
.map((value) => {
|
|
212
|
+
if (typeof value === 'string') return value;
|
|
213
|
+
try {
|
|
214
|
+
return JSON.stringify(value);
|
|
215
|
+
} catch {
|
|
216
|
+
return String(value);
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
.join('\n');
|
|
220
|
+
const match = text.match(/attached to workspace\s+([a-z0-9-]+)\.\s*Activate workspace\s+([a-z0-9-]+)\s+to switch/i);
|
|
221
|
+
if (!match) return null;
|
|
222
|
+
return {
|
|
223
|
+
attachedWorkspaceId: match[1],
|
|
224
|
+
requestedWorkspaceId: match[2],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function contextForAttachedWorkspaceMismatch(ctx, failure) {
|
|
229
|
+
const mismatch = extractAttachedWorkspaceMismatch(
|
|
230
|
+
failure?.result?.error,
|
|
231
|
+
failure?.result?.errorMessage,
|
|
232
|
+
failure?.result?.data,
|
|
233
|
+
failure?.fallback?.error,
|
|
234
|
+
failure?.fallback?.payload
|
|
235
|
+
);
|
|
236
|
+
if (!mismatch?.attachedWorkspaceId || mismatch.attachedWorkspaceId === ctx?.workspaceId) return null;
|
|
237
|
+
return { ...ctx, workspaceId: mismatch.attachedWorkspaceId };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function describeClaudeAuth(token, ctx) {
|
|
241
|
+
try {
|
|
242
|
+
const status = await fetchBusinessClaudeLoginStatus(token, ctx);
|
|
243
|
+
if (!status.ok) {
|
|
244
|
+
return {
|
|
245
|
+
connected: false,
|
|
246
|
+
label: 'Claude login: unknown',
|
|
247
|
+
detail: 'run /login to connect the remote computer',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const data = status.data || {};
|
|
251
|
+
if (data.loggedIn || data.connected || data.status === 'completed' || data.next_action === 'connected') {
|
|
252
|
+
return {
|
|
253
|
+
connected: true,
|
|
254
|
+
label: 'Claude login: connected',
|
|
255
|
+
detail: 'Claude subscription lane is active',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
connected: false,
|
|
260
|
+
label: 'Claude login: not connected',
|
|
261
|
+
detail: 'run /login to turn on the 0-credit Claude lane',
|
|
262
|
+
};
|
|
263
|
+
} catch {
|
|
264
|
+
return {
|
|
265
|
+
connected: false,
|
|
266
|
+
label: 'Claude login: unknown',
|
|
267
|
+
detail: 'run /login to connect the remote computer',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function describeBillingMode(token, ctx, worker) {
|
|
273
|
+
if (activeWorker(worker) === 'openai') {
|
|
274
|
+
return 'Atris credits';
|
|
275
|
+
}
|
|
276
|
+
const auth = await describeClaudeAuth(token, ctx);
|
|
277
|
+
if (auth.connected) {
|
|
278
|
+
return 'Claude subscription connected - 0 Atris credits';
|
|
279
|
+
}
|
|
280
|
+
return 'Claude via Atris credits - /login makes it 0 credits';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function printCloudHelp() {
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(ui.bold('Useful commands'));
|
|
286
|
+
console.log(' /start Show the beginner flow again');
|
|
287
|
+
console.log(' /help Show this menu');
|
|
288
|
+
console.log(' /status Show cloud computer status');
|
|
289
|
+
console.log(' /workflow Show the CodeOps workflow contract');
|
|
290
|
+
console.log(' /files [path] List files in the workspace');
|
|
291
|
+
console.log(' /run <cmd> Run shell without the model');
|
|
292
|
+
console.log(' /audit [n] Show recent runs, output, and charges');
|
|
293
|
+
console.log(' /worker claude Use Claude subscription lane');
|
|
294
|
+
console.log(' /worker openai Use OpenAI credit lane');
|
|
295
|
+
console.log(' /login Connect Claude subscription on the remote box');
|
|
296
|
+
console.log(' /reset Start a fresh chat session');
|
|
297
|
+
console.log(' /exit Leave cloud mode');
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log(ui.dim('No code needed: type the outcome in normal English. Unknown /commands are blocked locally.'));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function printCloudStartPanel(ctx, worker, model, billingLabel, authSummary = null) {
|
|
303
|
+
console.log('');
|
|
304
|
+
console.log(ui.bold('Atris Cloud Computer'));
|
|
305
|
+
console.log(`${ctx.businessName} ${ui.dim('/workspace persists')}`);
|
|
306
|
+
console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
|
|
307
|
+
console.log(`Billing: ${billingLabel}`);
|
|
308
|
+
if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
|
|
309
|
+
console.log(`${ui.green('Atris loaded')} ${ui.dim('plain English -> workspace actions')}`);
|
|
310
|
+
console.log('');
|
|
311
|
+
console.log(ui.bold('Start here'));
|
|
312
|
+
console.log(' Type what you want built. Atris can inspect, edit, run, and save files.');
|
|
313
|
+
console.log(' "look around this workspace and tell me what is here"');
|
|
314
|
+
console.log(' "build me a one-page website for my coffee shop"');
|
|
315
|
+
console.log(' "make a script that turns a CSV into a chart"');
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(ui.bold('Controls'));
|
|
318
|
+
console.log(' /start this screen /status lane, auth, billing');
|
|
319
|
+
console.log(' /files workspace files /run pwd shell without the model');
|
|
320
|
+
console.log(' /login connect Claude /worker openai use credits');
|
|
321
|
+
console.log(' /audit 5 recent runs /exit leave cloud mode');
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log(ui.dim('Plain English goes to Atris. Slash commands control the computer.'));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function appendSystemPrompt(basePrompt, extraPrompt) {
|
|
327
|
+
if (!extraPrompt) return basePrompt || null;
|
|
328
|
+
const marker = String(extraPrompt).split('\n', 1)[0];
|
|
329
|
+
if (basePrompt && marker && basePrompt.includes(marker)) return basePrompt;
|
|
330
|
+
if (!basePrompt) return extraPrompt;
|
|
331
|
+
return `${String(basePrompt).trim()}\n\n${extraPrompt}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function codeOpsCloudOptions(options = {}) {
|
|
335
|
+
return {
|
|
336
|
+
...options,
|
|
337
|
+
worker: options.worker || null,
|
|
338
|
+
mode: 'codeops',
|
|
339
|
+
systemPrompt: appendSystemPrompt(options.systemPrompt, CODEOPS_WORKFLOW_PROMPT),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function recruitingCloudOptions(options = {}) {
|
|
344
|
+
return {
|
|
345
|
+
...options,
|
|
346
|
+
worker: options.worker || null,
|
|
347
|
+
mode: 'recruiting',
|
|
348
|
+
systemPrompt: appendSystemPrompt(options.systemPrompt, RECRUITING_WORKFLOW_PROMPT),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary = null) {
|
|
353
|
+
console.log('');
|
|
354
|
+
console.log(ui.bold('Atris CodeOps Computer'));
|
|
355
|
+
console.log(`${ctx.businessName} ${ui.dim('/workspace persists, full permissions enabled')}`);
|
|
356
|
+
console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
|
|
357
|
+
console.log(`Billing: ${billingLabel}`);
|
|
358
|
+
if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
|
|
359
|
+
console.log(`${ui.green('Workflow locked')} ${ui.dim('observe -> plan -> act -> validate -> evidence -> next')}`);
|
|
360
|
+
console.log('');
|
|
361
|
+
console.log(ui.bold('Start here'));
|
|
362
|
+
console.log(' Type a coding goal in plain English.');
|
|
363
|
+
console.log(' CodeOps will plan first, then execute after approval or explicit proceed language.');
|
|
364
|
+
console.log(' Use /workflow to see the contract, /run for shell, /audit for run history, /exit to leave.');
|
|
365
|
+
console.log('');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function printCodeOpsWorkflowContract() {
|
|
369
|
+
console.log('');
|
|
370
|
+
console.log(ui.bold('CodeOps workflow'));
|
|
371
|
+
console.log(' observe -> hypothesis -> plan -> action -> validation -> evidence -> next state');
|
|
372
|
+
console.log('');
|
|
373
|
+
console.log(' Required final evidence: edited_files, commands_run, validation_result, pr_url, pr_state, merge_state, next_task.');
|
|
374
|
+
console.log(' Allowed states: planned, executing, validated, pr_opened, merge_ready, merge_blocked_checks, merge_blocked_policy, merged, failed, needs_human.');
|
|
375
|
+
console.log(' Full permissions stay on; the workflow contract controls how the computer uses them.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function printRecruitingWorkflowContract() {
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log(ui.bold('Recruiting workflow'));
|
|
381
|
+
console.log(' pipeline -> candidates -> next touch -> owner -> proof -> sync');
|
|
382
|
+
console.log('');
|
|
383
|
+
console.log(' No external outreach, DMs, emails, or calendar invites without explicit operator approval.');
|
|
384
|
+
console.log(' Required final evidence: pipeline touched, candidates reviewed, artifact changed, follow-up owner, sync/proof status.');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function printRecruitingComputerHelp() {
|
|
388
|
+
console.log('Usage: atris computer recruiting [chat|status|sync|doctor|pull|push|publish|watch|review|wake|sleep|run|grep|ls|cat|exec|audit|workflow|create]');
|
|
389
|
+
console.log('');
|
|
390
|
+
console.log('Examples:');
|
|
391
|
+
console.log(' atris computer recruiting');
|
|
392
|
+
console.log(' atris computer recruiting status');
|
|
393
|
+
console.log(' atris computer recruiting doctor');
|
|
394
|
+
console.log(' atris computer recruiting sync');
|
|
395
|
+
console.log(' atris computer recruiting pull');
|
|
396
|
+
console.log(' atris computer recruiting push --dry-run');
|
|
397
|
+
console.log(' atris computer recruiting publish --dry-run');
|
|
398
|
+
console.log(' atris computer recruiting watch');
|
|
399
|
+
console.log(' atris computer recruiting run "pwd && find . -maxdepth 2 -type f | head"');
|
|
400
|
+
console.log(" atris computer recruiting exec \"Summarize today's candidate follow-ups\"");
|
|
401
|
+
console.log(' atris computer recruiting create');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function displayHomeRelativePath(targetPath) {
|
|
405
|
+
const home = os.homedir();
|
|
406
|
+
if (targetPath && home && targetPath === home) return '~';
|
|
407
|
+
if (targetPath && home && targetPath.startsWith(`${home}${path.sep}`)) {
|
|
408
|
+
return `~${targetPath.slice(home.length)}`;
|
|
409
|
+
}
|
|
410
|
+
return targetPath;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function recruitingBusinessWorkspacePath(slug = RECRUITING_BUSINESS_SLUG) {
|
|
414
|
+
const root = businessWorkspaceBase();
|
|
415
|
+
return path.join(root, slug);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function normalizeBusinessSlug(value) {
|
|
419
|
+
return String(value || '')
|
|
420
|
+
.trim()
|
|
421
|
+
.toLowerCase()
|
|
422
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
423
|
+
.replace(/^-|-$/g, '');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function bindingMatchesBusinessSlug(binding, slug) {
|
|
427
|
+
if (!binding) return false;
|
|
428
|
+
const wanted = normalizeBusinessSlug(slug);
|
|
429
|
+
return [binding.slug, binding.business_slug, binding.name]
|
|
430
|
+
.map(normalizeBusinessSlug)
|
|
431
|
+
.some((candidate) => candidate && candidate === wanted);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function bindingBusinessLabel(binding) {
|
|
435
|
+
return binding?.slug || binding?.business_slug || binding?.name || 'unknown';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function resolveRecruitingSyncWorkspace(slug = RECRUITING_BUSINESS_SLUG) {
|
|
439
|
+
const currentBinding = readBusinessBinding();
|
|
440
|
+
if (bindingMatchesBusinessSlug(currentBinding, slug)) {
|
|
441
|
+
return {
|
|
442
|
+
cwd: process.cwd(),
|
|
443
|
+
binding: currentBinding,
|
|
444
|
+
source: 'current',
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const canonicalCwd = recruitingBusinessWorkspacePath(slug);
|
|
449
|
+
const canonicalBinding = readBusinessBinding(canonicalCwd);
|
|
450
|
+
if (bindingMatchesBusinessSlug(canonicalBinding, slug)) {
|
|
451
|
+
return {
|
|
452
|
+
cwd: canonicalCwd,
|
|
453
|
+
binding: canonicalBinding,
|
|
454
|
+
source: 'canonical',
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function printRecruitingSyncNextSteps(slug = RECRUITING_BUSINESS_SLUG, workspacePath = null) {
|
|
462
|
+
const target = workspacePath || recruitingBusinessWorkspacePath(slug);
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log('Recruiting sync commands');
|
|
465
|
+
console.log(` cd ${displayHomeRelativePath(target)}`);
|
|
466
|
+
console.log(' atris computer recruiting doctor');
|
|
467
|
+
console.log(' atris computer recruiting pull');
|
|
468
|
+
console.log(' atris computer recruiting push --dry-run');
|
|
469
|
+
console.log(' atris computer recruiting publish --dry-run');
|
|
470
|
+
console.log(' atris sync --status');
|
|
471
|
+
console.log(' atris sync --dry-run');
|
|
472
|
+
console.log(' atris sync');
|
|
473
|
+
console.log(' atris sync --watch');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function recruitingLocalSyncCommand(action, slug = RECRUITING_BUSINESS_SLUG, args = []) {
|
|
477
|
+
const withFlag = (items, flag) => items.includes(flag) ? items : [...items, flag];
|
|
478
|
+
switch (action) {
|
|
479
|
+
case 'pull':
|
|
480
|
+
return ['pull', slug, '--keep-local', '--fail-on-conflict', ...args];
|
|
481
|
+
case 'push':
|
|
482
|
+
return ['push', slug, ...args];
|
|
483
|
+
case 'publish':
|
|
484
|
+
return ['push', slug, ...withFlag(args, '--allow-broad-workspace')];
|
|
485
|
+
case 'watch':
|
|
486
|
+
return ['sync', '--watch', ...args];
|
|
487
|
+
case 'review':
|
|
488
|
+
return ['sync', '--review', ...args];
|
|
489
|
+
case 'doctor':
|
|
490
|
+
return ['sync', '--status', ...args];
|
|
491
|
+
default:
|
|
492
|
+
return ['sync', ...args];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function printRecruitingLocalSyncCommandHelp(action, slug = RECRUITING_BUSINESS_SLUG) {
|
|
497
|
+
const command = recruitingLocalSyncCommand(action, slug, (action === 'push' || action === 'publish') ? ['--dry-run'] : []);
|
|
498
|
+
console.log(`Usage: atris computer recruiting ${action} [flags]`);
|
|
499
|
+
console.log('');
|
|
500
|
+
console.log('Runs from the current or canonical Atris Labs recruiting workspace.');
|
|
501
|
+
if (action === 'pull') {
|
|
502
|
+
console.log('Use --dry-run first; use --apply to write into a dirty local workspace.');
|
|
503
|
+
} else if (action === 'push') {
|
|
504
|
+
console.log('Use --dry-run first; after reviewing a broad publish, add --allow-broad-workspace.');
|
|
505
|
+
} else if (action === 'publish') {
|
|
506
|
+
console.log('Reviewed publish shortcut. Use --dry-run first, then run without --dry-run.');
|
|
507
|
+
}
|
|
508
|
+
console.log('');
|
|
509
|
+
console.log('Underlying command:');
|
|
510
|
+
console.log(` atris ${command.join(' ')}`);
|
|
511
|
+
if (action === 'push') {
|
|
512
|
+
console.log('');
|
|
513
|
+
console.log('Reviewed broad publish:');
|
|
514
|
+
console.log(' atris computer recruiting push --dry-run --allow-broad-workspace');
|
|
515
|
+
console.log(' atris computer recruiting push --allow-broad-workspace');
|
|
516
|
+
} else if (action === 'publish') {
|
|
517
|
+
console.log('');
|
|
518
|
+
console.log('Reviewed publish:');
|
|
519
|
+
console.log(' atris computer recruiting publish --dry-run');
|
|
520
|
+
console.log(' atris computer recruiting publish');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function withoutRecruitingWrapperFlags(action, args = []) {
|
|
525
|
+
if (action !== 'pull') return args;
|
|
526
|
+
return args.filter((arg) => arg !== '--apply');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function gitDirtySummary(cwd) {
|
|
530
|
+
const result = spawnSync('git', ['-C', cwd, 'status', '--short'], {
|
|
531
|
+
encoding: 'utf8',
|
|
532
|
+
env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1' },
|
|
533
|
+
});
|
|
534
|
+
if (result.status !== 0) return null;
|
|
535
|
+
const lines = String(result.stdout || '').split(/\r?\n/).filter(Boolean);
|
|
536
|
+
return {
|
|
537
|
+
count: lines.length,
|
|
538
|
+
sample: lines.slice(0, 5),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function gitOutput(cwd, args = []) {
|
|
543
|
+
const result = spawnSync('git', ['-C', cwd, ...args], {
|
|
544
|
+
encoding: 'utf8',
|
|
545
|
+
env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1' },
|
|
546
|
+
});
|
|
547
|
+
if (result.status !== 0) return '';
|
|
548
|
+
return String(result.stdout || '');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function recruitingCheckpointStamp() {
|
|
552
|
+
return (process.env.ATRIS_RECRUITING_CHECKPOINT_STAMP || new Date().toISOString())
|
|
553
|
+
.replace(/[-:]/g, '')
|
|
554
|
+
.replace(/\.\d{3}Z$/, 'Z');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function listRecruitingUntrackedFiles(cwd) {
|
|
558
|
+
return gitOutput(cwd, ['ls-files', '--others', '--exclude-standard'])
|
|
559
|
+
.split(/\r?\n/)
|
|
560
|
+
.map((line) => line.trim())
|
|
561
|
+
.filter(Boolean)
|
|
562
|
+
.filter((filePath) => !filePath.startsWith('.atris/sync/checkpoints/'));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function copyRecruitingUntrackedFiles(cwd, checkpointDir, untrackedFiles = []) {
|
|
566
|
+
let copied = 0;
|
|
567
|
+
const targetRoot = path.join(checkpointDir, 'untracked');
|
|
568
|
+
for (const filePath of untrackedFiles) {
|
|
569
|
+
const source = path.join(cwd, filePath);
|
|
570
|
+
const target = path.join(targetRoot, filePath);
|
|
571
|
+
try {
|
|
572
|
+
if (!fs.statSync(source).isFile()) continue;
|
|
573
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
574
|
+
fs.copyFileSync(source, target);
|
|
575
|
+
copied++;
|
|
576
|
+
} catch {
|
|
577
|
+
// Some files move while tools write state; keep the checkpoint best-effort.
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return copied;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function writeRecruitingPullCheckpoint(cwd, dirty) {
|
|
584
|
+
if (!dirty || dirty.count === 0) return null;
|
|
585
|
+
|
|
586
|
+
const stamp = recruitingCheckpointStamp();
|
|
587
|
+
const relDir = `.atris/sync/checkpoints/${stamp}`;
|
|
588
|
+
const checkpointDir = path.join(cwd, relDir);
|
|
589
|
+
const untrackedFiles = listRecruitingUntrackedFiles(cwd);
|
|
590
|
+
fs.mkdirSync(checkpointDir, { recursive: true });
|
|
591
|
+
|
|
592
|
+
const trackedPatch = gitOutput(cwd, ['diff', '--binary']);
|
|
593
|
+
const stagedPatch = gitOutput(cwd, ['diff', '--cached', '--binary']);
|
|
594
|
+
fs.writeFileSync(path.join(checkpointDir, 'tracked.patch'), trackedPatch, 'utf8');
|
|
595
|
+
fs.writeFileSync(path.join(checkpointDir, 'staged.patch'), stagedPatch, 'utf8');
|
|
596
|
+
fs.writeFileSync(path.join(checkpointDir, 'untracked.txt'), `${untrackedFiles.join('\n')}${untrackedFiles.length ? '\n' : ''}`, 'utf8');
|
|
597
|
+
const copiedUntracked = copyRecruitingUntrackedFiles(cwd, checkpointDir, untrackedFiles);
|
|
598
|
+
|
|
599
|
+
const summary = [
|
|
600
|
+
'# Recruiting Pull Checkpoint',
|
|
601
|
+
'',
|
|
602
|
+
`created_at: ${new Date().toISOString()}`,
|
|
603
|
+
`workspace: ${cwd}`,
|
|
604
|
+
`git_status_count: ${dirty.count}`,
|
|
605
|
+
`untracked_files_listed: ${untrackedFiles.length}`,
|
|
606
|
+
`untracked_files_copied: ${copiedUntracked}`,
|
|
607
|
+
'',
|
|
608
|
+
'Sample status:',
|
|
609
|
+
...dirty.sample.map((line) => `- ${line}`),
|
|
610
|
+
'',
|
|
611
|
+
'Restore hints:',
|
|
612
|
+
`- git apply ${relDir}/tracked.patch`,
|
|
613
|
+
`- git apply --cached ${relDir}/staged.patch`,
|
|
614
|
+
`- cp -R ${relDir}/untracked/. .`,
|
|
615
|
+
'',
|
|
616
|
+
].join('\n');
|
|
617
|
+
fs.writeFileSync(path.join(checkpointDir, 'summary.md'), summary, 'utf8');
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
relDir,
|
|
621
|
+
summary: `${relDir}/summary.md`,
|
|
622
|
+
untrackedFiles: untrackedFiles.length,
|
|
623
|
+
copiedUntracked,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function printRecruitingPullPreflight(summary) {
|
|
628
|
+
console.log('');
|
|
629
|
+
console.log('Recruiting pull preflight');
|
|
630
|
+
console.log(` local git changes: ${summary.count}`);
|
|
631
|
+
summary.sample.forEach((line) => console.log(` ${line}`));
|
|
632
|
+
console.log('');
|
|
633
|
+
console.log('Safe next step');
|
|
634
|
+
console.log(' atris computer recruiting pull --dry-run');
|
|
635
|
+
console.log(' atris computer recruiting pull --apply # writes pull changes and conflict review packet');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function latestRecruitingConflictPacket(cwd) {
|
|
639
|
+
const conflictsRoot = path.join(cwd, '.atris', 'sync', 'conflicts');
|
|
640
|
+
const summaries = [];
|
|
641
|
+
|
|
642
|
+
function walk(dir) {
|
|
643
|
+
let entries;
|
|
644
|
+
try {
|
|
645
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
646
|
+
} catch {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
for (const entry of entries) {
|
|
650
|
+
const full = path.join(dir, entry.name);
|
|
651
|
+
if (entry.isDirectory()) walk(full);
|
|
652
|
+
else if (entry.isFile() && entry.name === 'summary.md') {
|
|
653
|
+
summaries.push(path.relative(cwd, full).replace(/\\/g, '/'));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
walk(conflictsRoot);
|
|
659
|
+
summaries.sort();
|
|
660
|
+
return summaries[summaries.length - 1] || null;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function latestRecruitingCheckpoint(cwd) {
|
|
664
|
+
const checkpointRoot = path.join(cwd, '.atris', 'sync', 'checkpoints');
|
|
665
|
+
const summaries = [];
|
|
666
|
+
|
|
667
|
+
function walk(dir) {
|
|
668
|
+
let entries;
|
|
669
|
+
try {
|
|
670
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
671
|
+
} catch {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
for (const entry of entries) {
|
|
675
|
+
const full = path.join(dir, entry.name);
|
|
676
|
+
if (entry.isDirectory()) walk(full);
|
|
677
|
+
else if (entry.isFile() && entry.name === 'summary.md') {
|
|
678
|
+
summaries.push(path.relative(cwd, full).replace(/\\/g, '/'));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
walk(checkpointRoot);
|
|
684
|
+
summaries.sort();
|
|
685
|
+
return summaries[summaries.length - 1] || null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function printRecruitingSyncDoctor(workspace, slug = RECRUITING_BUSINESS_SLUG) {
|
|
689
|
+
const dirty = gitDirtySummary(workspace.cwd);
|
|
690
|
+
const latestPacket = latestRecruitingConflictPacket(workspace.cwd);
|
|
691
|
+
const latestCheckpoint = latestRecruitingCheckpoint(workspace.cwd);
|
|
692
|
+
console.log('Recruiting sync doctor');
|
|
693
|
+
console.log(` workspace: ${displayHomeRelativePath(workspace.cwd)}`);
|
|
694
|
+
console.log(` business: ${bindingBusinessLabel(workspace.binding) || slug}`);
|
|
695
|
+
if (dirty) {
|
|
696
|
+
console.log(` local git changes: ${dirty.count}`);
|
|
697
|
+
dirty.sample.forEach((line) => console.log(` ${line}`));
|
|
698
|
+
} else {
|
|
699
|
+
console.log(' local git changes: unknown');
|
|
700
|
+
}
|
|
701
|
+
console.log(` review packet: ${latestPacket || 'none'}`);
|
|
702
|
+
console.log(` latest checkpoint: ${latestCheckpoint || 'none'}`);
|
|
703
|
+
console.log('');
|
|
704
|
+
console.log('Next command');
|
|
705
|
+
if (latestPacket) {
|
|
706
|
+
console.log(' atris computer recruiting review');
|
|
707
|
+
} else if (dirty && dirty.count > 0) {
|
|
708
|
+
console.log(' atris computer recruiting pull --dry-run');
|
|
709
|
+
} else {
|
|
710
|
+
console.log(' atris computer recruiting pull --dry-run');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function runAtrisCliCommand(cliArgs, cwd) {
|
|
715
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'atris.js'), ...cliArgs], {
|
|
716
|
+
cwd,
|
|
717
|
+
stdio: 'inherit',
|
|
718
|
+
env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1' },
|
|
719
|
+
});
|
|
720
|
+
if (result.error) throw result.error;
|
|
721
|
+
if (result.signal) {
|
|
722
|
+
process.exitCode = 1;
|
|
723
|
+
return 1;
|
|
724
|
+
}
|
|
725
|
+
process.exitCode = result.status || 0;
|
|
726
|
+
return process.exitCode;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function printRecruitingLocalSyncOutcome(action, status = 0, args = []) {
|
|
730
|
+
const isPublishLike = action === 'push' || action === 'publish';
|
|
731
|
+
|
|
732
|
+
if (action === 'pull') {
|
|
733
|
+
console.log('');
|
|
734
|
+
console.log('Recruiting next step');
|
|
735
|
+
if (args.includes('--dry-run')) {
|
|
736
|
+
console.log(' atris computer recruiting pull --apply # writes review packet if conflicts were reported');
|
|
737
|
+
console.log(' atris computer recruiting review');
|
|
738
|
+
console.log(' atris computer recruiting push --dry-run');
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
console.log(' atris computer recruiting review # if conflicts were reported');
|
|
742
|
+
console.log(' atris computer recruiting push --dry-run');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (isPublishLike && status !== 0) {
|
|
747
|
+
console.log('');
|
|
748
|
+
console.log('Recruiting next step');
|
|
749
|
+
console.log(' If the output says review before publish:');
|
|
750
|
+
console.log(' atris computer recruiting publish --dry-run');
|
|
751
|
+
console.log(' atris computer recruiting publish');
|
|
752
|
+
console.log(' Otherwise:');
|
|
753
|
+
console.log(' atris computer recruiting pull --dry-run');
|
|
754
|
+
console.log(' atris computer recruiting review # if conflicts were reported');
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (action === 'publish' && args.includes('--dry-run')) {
|
|
759
|
+
console.log('');
|
|
760
|
+
console.log('Recruiting next step');
|
|
761
|
+
console.log(' atris computer recruiting publish');
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (action === 'push' && args.includes('--dry-run')) {
|
|
766
|
+
console.log('');
|
|
767
|
+
console.log('Recruiting next step');
|
|
768
|
+
if (args.includes('--allow-broad-workspace')) {
|
|
769
|
+
console.log(' atris computer recruiting push --allow-broad-workspace');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
console.log(' If the dry-run showed review before publish:');
|
|
773
|
+
console.log(' atris computer recruiting push --dry-run --allow-broad-workspace');
|
|
774
|
+
console.log(' atris computer recruiting push --allow-broad-workspace');
|
|
775
|
+
console.log(' Otherwise:');
|
|
776
|
+
console.log(' atris computer recruiting push');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (action === 'review') {
|
|
780
|
+
console.log('');
|
|
781
|
+
console.log('Review note');
|
|
782
|
+
console.log(' If pull --dry-run reported conflicts but no packet appears:');
|
|
783
|
+
console.log(' atris computer recruiting pull --apply');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function runRecruitingLocalSyncCommand(action, args = [], cloudOptions = {}) {
|
|
788
|
+
const slug = cloudOptions.businessSlug || RECRUITING_BUSINESS_SLUG;
|
|
789
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
790
|
+
printRecruitingLocalSyncCommandHelp(action, slug);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const workspace = resolveRecruitingSyncWorkspace(slug);
|
|
795
|
+
console.log(`Recruiting local ${action}`);
|
|
796
|
+
if (!workspace) {
|
|
797
|
+
const currentBinding = readBusinessBinding();
|
|
798
|
+
if (currentBinding && !bindingMatchesBusinessSlug(currentBinding, slug)) {
|
|
799
|
+
console.log(` current workspace: ${bindingBusinessLabel(currentBinding)} (not ${slug})`);
|
|
800
|
+
} else {
|
|
801
|
+
console.log(' local workspace: not detected in this folder');
|
|
802
|
+
}
|
|
803
|
+
printRecruitingLocalSyncCommandHelp(action, slug);
|
|
804
|
+
printRecruitingSyncNextSteps(slug);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (workspace.source === 'canonical') {
|
|
809
|
+
console.log(` folder: ${displayHomeRelativePath(workspace.cwd)} (auto-detected)`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (action === 'doctor') {
|
|
813
|
+
printRecruitingSyncDoctor(workspace, slug);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const commandArgs = withoutRecruitingWrapperFlags(action, args);
|
|
818
|
+
if (action === 'pull' && !args.includes('--dry-run') && !args.includes('--apply')) {
|
|
819
|
+
const dirty = gitDirtySummary(workspace.cwd);
|
|
820
|
+
if (dirty && dirty.count > 0) {
|
|
821
|
+
printRecruitingPullPreflight(dirty);
|
|
822
|
+
process.exitCode = 1;
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (action === 'pull' && args.includes('--apply')) {
|
|
827
|
+
const dirty = gitDirtySummary(workspace.cwd);
|
|
828
|
+
const checkpoint = writeRecruitingPullCheckpoint(workspace.cwd, dirty);
|
|
829
|
+
if (checkpoint) {
|
|
830
|
+
console.log('');
|
|
831
|
+
console.log('Recruiting checkpoint');
|
|
832
|
+
console.log(` wrote: ${checkpoint.summary}`);
|
|
833
|
+
console.log(` untracked copied: ${checkpoint.copiedUntracked}/${checkpoint.untrackedFiles}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const command = recruitingLocalSyncCommand(action, workspace.binding.slug || slug, commandArgs);
|
|
838
|
+
const status = runAtrisCliCommand(command, workspace.cwd);
|
|
839
|
+
printRecruitingLocalSyncOutcome(action, status, commandArgs);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function runRecruitingSyncHelper(args = [], cloudOptions = {}) {
|
|
843
|
+
const slug = cloudOptions.businessSlug || RECRUITING_BUSINESS_SLUG;
|
|
844
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
845
|
+
console.log('Usage: atris computer recruiting sync [--status|--dry-run|--watch|--review|--resolve local|cloud|merge]');
|
|
846
|
+
console.log('');
|
|
847
|
+
console.log('Runs local recruiting workspace sync from the current or canonical Atris Labs folder.');
|
|
848
|
+
printRecruitingSyncNextSteps(slug);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const workspace = resolveRecruitingSyncWorkspace(slug);
|
|
853
|
+
console.log('Recruiting local sync');
|
|
854
|
+
if (!workspace) {
|
|
855
|
+
const currentBinding = readBusinessBinding();
|
|
856
|
+
if (currentBinding && !bindingMatchesBusinessSlug(currentBinding, slug)) {
|
|
857
|
+
console.log(` current workspace: ${bindingBusinessLabel(currentBinding)} (not ${slug})`);
|
|
858
|
+
} else {
|
|
859
|
+
console.log(' local workspace: not detected in this folder');
|
|
860
|
+
}
|
|
861
|
+
printRecruitingSyncNextSteps(slug);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (workspace.source === 'canonical') {
|
|
866
|
+
console.log(` folder: ${displayHomeRelativePath(workspace.cwd)} (auto-detected)`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const { businessSync } = require('../commands/business-sync');
|
|
870
|
+
await businessSync(args.length > 0 ? args : ['--status'], workspace.cwd);
|
|
871
|
+
printRecruitingSyncNextSteps(workspace.binding.slug || slug, workspace.cwd);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function buildLocalBridgeSystemPrompt(sessionId, localRoot, allowBash) {
|
|
875
|
+
const endpoint = `/api/cli/sessions/${sessionId}/file-op`;
|
|
876
|
+
const bashLine = allowBash
|
|
877
|
+
? '- Run local commands with local_file_op({ "type": "bash", "command": "..." }).'
|
|
878
|
+
: '- Bash is disabled for this local session. Use read/write/edit/delete only.';
|
|
879
|
+
|
|
880
|
+
return `
|
|
881
|
+
|
|
882
|
+
## Atris Local Folder Mode
|
|
883
|
+
|
|
884
|
+
The user connected their LOCAL folder to Atris through CLI session ${sessionId}.
|
|
885
|
+
Their local root is: ${localRoot}
|
|
886
|
+
Treat this local folder as the primary workspace for this chat.
|
|
887
|
+
The cloud /workspace is only a control plane.
|
|
888
|
+
Do not use Write/Edit/apply_patch for requested local edits.
|
|
889
|
+
Use the native local_file_op tool for every local filesystem change.
|
|
890
|
+
|
|
891
|
+
Preferred tool calls:
|
|
892
|
+
- local_file_op({ "type": "read", "path": "relative/path.txt" })
|
|
893
|
+
- local_file_op({ "type": "write", "path": "file.txt", "content": "..." })
|
|
894
|
+
- local_file_op({ "type": "edit", "path": "file.txt", "find": "...", "replace": "..." })
|
|
895
|
+
- local_file_op({ "type": "delete", "path": "file.txt" })
|
|
896
|
+
${bashLine}
|
|
897
|
+
|
|
898
|
+
Fallback if the native tool is unavailable: use Bash to call the Atris Python API from the cloud workspace:
|
|
899
|
+
|
|
900
|
+
\`\`\`python
|
|
901
|
+
from atris_api import api
|
|
902
|
+
api("POST", "${endpoint}", {
|
|
903
|
+
"type": "read",
|
|
904
|
+
"path": "relative/path.txt",
|
|
905
|
+
"wait_for_ack": True,
|
|
906
|
+
"timeout_seconds": 30,
|
|
907
|
+
})
|
|
908
|
+
\`\`\`
|
|
909
|
+
|
|
910
|
+
Supported operations:
|
|
911
|
+
- Read: { "type": "read", "path": "file.txt", "wait_for_ack": true }
|
|
912
|
+
- Write: { "type": "write", "path": "file.txt", "content": "...", "wait_for_ack": true }
|
|
913
|
+
- Edit: { "type": "edit", "path": "file.txt", "find": "...", "replace": "...", "wait_for_ack": true }
|
|
914
|
+
- Delete: { "type": "delete", "path": "file.txt", "wait_for_ack": true }
|
|
915
|
+
|
|
916
|
+
Rules:
|
|
917
|
+
- All paths must be relative to the local root.
|
|
918
|
+
- Read before editing unless you are creating a new file.
|
|
919
|
+
- Use local bash for ls/rg/tests when available.
|
|
920
|
+
- Do not ask the user to copy, paste, or save files. Apply the change through the bridge.
|
|
921
|
+
- In final answers, say what changed locally and how you verified it.
|
|
922
|
+
---`;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function printLocalAtrisStartPanel(ctx, bridge, worker, model, billingLabel, authSummary = null) {
|
|
926
|
+
console.log('');
|
|
927
|
+
console.log(ui.bold('Atris Local Computer'));
|
|
928
|
+
console.log(`${ctx.businessName} ${ui.dim('cloud brain -> local folder')}`);
|
|
929
|
+
console.log(`Local: ${bridge.workingDir}`);
|
|
930
|
+
console.log(`Bridge: ${bridge.sessionId.slice(0, 8)} ${ui.dim(bridge.allowBash ? 'local bash enabled' : 'file ops only')}`);
|
|
931
|
+
console.log(`Lane: ${ui.bold(formatWorkerName(worker))} ${ui.dim(formatCloudSelection({ worker, model }))}`);
|
|
932
|
+
console.log(`Billing: ${billingLabel}`);
|
|
933
|
+
if (authSummary) console.log(`${authSummary.label} ${ui.dim(authSummary.detail)}`);
|
|
934
|
+
console.log(`${ui.green('Atris loaded')} ${ui.dim('plain English -> local edits')}`);
|
|
935
|
+
console.log('');
|
|
936
|
+
console.log(ui.bold('Start here'));
|
|
937
|
+
console.log(' "look around this folder and tell me what is here"');
|
|
938
|
+
console.log(' "make the homepage look premium"');
|
|
939
|
+
console.log(' "add a script that converts a CSV into a chart"');
|
|
940
|
+
console.log('');
|
|
941
|
+
console.log(ui.bold('Controls'));
|
|
942
|
+
console.log(' /status local bridge, lane, billing');
|
|
943
|
+
console.log(' /files local files');
|
|
944
|
+
console.log(' /run local shell command');
|
|
945
|
+
console.log(' /audit recent cloud brain runs');
|
|
946
|
+
console.log(' /worker claude|openai');
|
|
947
|
+
console.log(' /exit leave local Atris mode');
|
|
948
|
+
console.log('');
|
|
949
|
+
console.log(ui.dim('Tokens run through Atris/cloud billing. Edits land in this local folder.'));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async function printCloudSessionStatus(token, ctx, worker, model) {
|
|
953
|
+
const statusResult = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, {
|
|
954
|
+
method: 'GET',
|
|
955
|
+
token,
|
|
956
|
+
});
|
|
957
|
+
const d = statusResult.ok ? (statusResult.data || {}) : {};
|
|
958
|
+
const computerState = d.status || (statusResult.ok ? 'unknown' : `error ${statusResult.status}`);
|
|
959
|
+
const authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
|
|
960
|
+
const billingLabel = await describeBillingMode(token, ctx, worker);
|
|
961
|
+
|
|
962
|
+
console.log('');
|
|
963
|
+
console.log(ui.bold('Cloud status'));
|
|
964
|
+
console.log(` Computer: ${computerState}`);
|
|
965
|
+
console.log(` Business: ${ctx.businessName}`);
|
|
966
|
+
console.log(' Workspace: /workspace');
|
|
967
|
+
console.log(` Lane: ${formatWorkerName(worker)} ${formatCloudSelection({ worker, model })}`);
|
|
968
|
+
console.log(` Billing: ${billingLabel}`);
|
|
969
|
+
console.log(' Atris: loaded');
|
|
970
|
+
if (authSummary) console.log(` Claude: ${authSummary.connected ? 'connected' : 'not connected'} ${authSummary.detail}`);
|
|
971
|
+
if (d.endpoint) console.log(` Endpoint: ${d.endpoint}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function questionAsync(rl, question) {
|
|
975
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async function selectFromDropdown(title, choices) {
|
|
979
|
+
if (!useInteractiveTerminalUi() || !choices.length) return choices[0] || null;
|
|
980
|
+
|
|
981
|
+
console.log(ui.bold(title));
|
|
982
|
+
choices.forEach((choice, i) => {
|
|
983
|
+
console.log(`${i + 1}. ${choice.label} ${ui.dim(choice.detail || '')}`.trimEnd());
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
987
|
+
const answer = String(await questionAsync(rl, `Choose [1-${choices.length}] (default 1): `) || '').trim();
|
|
988
|
+
rl.close();
|
|
989
|
+
|
|
990
|
+
if (!answer) return choices[0];
|
|
991
|
+
if (answer.toLowerCase() === 'q' || answer.toLowerCase() === 'quit' || answer.toLowerCase() === 'exit') {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const selected = Number.parseInt(answer, 10);
|
|
995
|
+
if (Number.isFinite(selected) && selected >= 1 && selected <= choices.length) {
|
|
996
|
+
return choices[selected - 1];
|
|
997
|
+
}
|
|
998
|
+
return choices[0];
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function chooseComputerSurface(hasBusinessBinding, hasLocalHarness) {
|
|
1002
|
+
if (!useInteractiveTerminalUi()) {
|
|
1003
|
+
return hasBusinessBinding ? 'cloud' : 'local';
|
|
1004
|
+
}
|
|
1005
|
+
if (hasBusinessBinding) {
|
|
1006
|
+
const choices = [
|
|
1007
|
+
{ label: 'Cloud workspace', value: 'cloud', detail: '/workspace, shared, Atris loaded' },
|
|
1008
|
+
{ label: 'Local folder', value: 'local-atris', detail: 'edits this folder, tokens run through Atris' },
|
|
1009
|
+
];
|
|
1010
|
+
if (hasLocalHarness) {
|
|
1011
|
+
choices.push({ label: 'Local BYO Claude', value: 'local-byo', detail: 'advanced, tokens go to Anthropic' });
|
|
1012
|
+
}
|
|
1013
|
+
const selected = await selectFromDropdown('Choose computer', choices);
|
|
1014
|
+
if (selected === null) return null;
|
|
1015
|
+
return selected?.value || 'cloud';
|
|
1016
|
+
}
|
|
1017
|
+
return 'local';
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function chooseCloudLane(token, ctx, initialOptions = {}) {
|
|
1021
|
+
let worker = initialOptions.worker || ctx?.worker || null;
|
|
1022
|
+
let model = initialOptions.model || null;
|
|
1023
|
+
|
|
1024
|
+
if (!worker && useInteractiveCloudUi()) {
|
|
1025
|
+
const selected = await selectFromDropdown('Choose compute lane', [
|
|
1026
|
+
{ label: 'Claude', value: 'claude', detail: 'subscription lane when connected, 0 Atris credits' },
|
|
1027
|
+
{ label: 'OpenAI', value: 'openai', detail: 'works now, uses Atris credits' },
|
|
1028
|
+
]);
|
|
1029
|
+
if (selected === null) return { cancelled: true };
|
|
1030
|
+
if (selected?.value) worker = selected.value;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (activeWorker(worker) === 'claude' && useInteractiveCloudUi()) {
|
|
1034
|
+
let state = null;
|
|
1035
|
+
try {
|
|
1036
|
+
const status = await fetchBusinessClaudeLoginStatus(token, ctx);
|
|
1037
|
+
state = status.ok ? status.data : null;
|
|
1038
|
+
} catch {
|
|
1039
|
+
state = null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (!state?.connected && !state?.loggedIn && state?.status !== 'completed' && state?.next_action !== 'connected') {
|
|
1043
|
+
const selected = await selectFromDropdown('Claude subscription auth', [
|
|
1044
|
+
{ label: 'Use Atris Claude', value: 'continue', detail: 'works now, uses Atris credits' },
|
|
1045
|
+
{ label: 'Login to Claude', value: 'login', detail: 'turns on 0-credit Claude lane' },
|
|
1046
|
+
{ label: 'Use OpenAI', value: 'openai', detail: 'works now, uses Atris credits' },
|
|
1047
|
+
]);
|
|
1048
|
+
if (selected === null) return { cancelled: true };
|
|
1049
|
+
if (selected?.value === 'login') {
|
|
1050
|
+
await computerCloudLogin(token, ctx);
|
|
1051
|
+
} else if (selected?.value === 'openai') {
|
|
1052
|
+
worker = 'openai';
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return { worker, model };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function parseComputerOptions(argv) {
|
|
1061
|
+
const positional = [];
|
|
1062
|
+
let worker = process.env.ATRIS_CLOUD_WORKER || null;
|
|
1063
|
+
let model = process.env.ATRIS_CLOUD_MODEL || null;
|
|
1064
|
+
let businessSlug = null;
|
|
1065
|
+
let workspaceId = null;
|
|
1066
|
+
let waitForResult = true;
|
|
1067
|
+
let message = null;
|
|
1068
|
+
let force = false;
|
|
1069
|
+
|
|
1070
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1071
|
+
const arg = argv[i];
|
|
1072
|
+
if ((arg === '--business' || arg === '-b') && argv[i + 1]) {
|
|
1073
|
+
businessSlug = argv[i + 1];
|
|
1074
|
+
i++;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (arg.startsWith('--business=')) {
|
|
1078
|
+
businessSlug = arg.split('=', 2)[1] || null;
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if ((arg === '--workspace' || arg === '--workspace-id') && argv[i + 1]) {
|
|
1082
|
+
workspaceId = argv[i + 1];
|
|
1083
|
+
i++;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (arg.startsWith('--workspace=')) {
|
|
1087
|
+
workspaceId = arg.split('=', 2)[1] || null;
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (arg.startsWith('--workspace-id=')) {
|
|
1091
|
+
workspaceId = arg.split('=', 2)[1] || null;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (arg === '--worker' && argv[i + 1]) {
|
|
1095
|
+
worker = argv[i + 1];
|
|
1096
|
+
i++;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
if (arg.startsWith('--worker=')) {
|
|
1100
|
+
worker = arg.split('=', 2)[1] || null;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (arg === '--model' && argv[i + 1]) {
|
|
1104
|
+
model = argv[i + 1];
|
|
1105
|
+
i++;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (arg.startsWith('--model=')) {
|
|
1109
|
+
model = arg.split('=', 2)[1] || null;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (arg === '--message' && argv[i + 1]) {
|
|
1113
|
+
message = argv[i + 1];
|
|
1114
|
+
i++;
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (arg.startsWith('--message=')) {
|
|
1118
|
+
message = arg.slice('--message='.length);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (arg === '--no-wait' || arg === '--async') {
|
|
1122
|
+
waitForResult = false;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (arg === '--force') {
|
|
1126
|
+
force = true;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
positional.push(arg);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (worker && !VALID_CLOUD_WORKERS.has(worker)) {
|
|
1133
|
+
console.error(`Invalid cloud worker: ${worker}`);
|
|
1134
|
+
console.error('Expected one of: claude, openai');
|
|
1135
|
+
process.exit(1);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return {
|
|
1139
|
+
positional,
|
|
1140
|
+
options: {
|
|
1141
|
+
worker: worker || null,
|
|
1142
|
+
model: model || null,
|
|
1143
|
+
businessSlug: businessSlug ? String(businessSlug).trim() : null,
|
|
1144
|
+
workspaceId: workspaceId ? String(workspaceId).trim() : null,
|
|
1145
|
+
waitForResult,
|
|
1146
|
+
message,
|
|
1147
|
+
force,
|
|
1148
|
+
},
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function parseComputerCreateArgs(argv = []) {
|
|
1153
|
+
const nameParts = [];
|
|
1154
|
+
let businessSlug = null;
|
|
1155
|
+
let help = false;
|
|
1156
|
+
let setDefault = false;
|
|
1157
|
+
let computerType = null;
|
|
1158
|
+
|
|
1159
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1160
|
+
const arg = argv[i];
|
|
1161
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
1162
|
+
help = true;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if ((arg === '--business' || arg === '-b') && argv[i + 1]) {
|
|
1166
|
+
businessSlug = argv[i + 1];
|
|
1167
|
+
i++;
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (arg.startsWith('--business=')) {
|
|
1171
|
+
businessSlug = arg.split('=', 2)[1] || null;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if ((arg === '--type' || arg === '-t') && argv[i + 1] && !argv[i + 1].startsWith('-')) {
|
|
1175
|
+
computerType = argv[i + 1];
|
|
1176
|
+
i++;
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (arg.startsWith('--type=')) {
|
|
1180
|
+
computerType = arg.split('=', 2)[1] || null;
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (arg === '--set-default') {
|
|
1184
|
+
setDefault = true;
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
nameParts.push(arg);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return {
|
|
1191
|
+
name: nameParts.join(' ').trim(),
|
|
1192
|
+
businessSlug: businessSlug ? String(businessSlug).trim() : null,
|
|
1193
|
+
computerType: computerType ? normalizeComputerType(computerType) : null,
|
|
1194
|
+
help,
|
|
1195
|
+
setDefault,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function computerCreateArgsHaveName(argv = []) {
|
|
1200
|
+
const flagsWithValues = new Set(['--business', '-b', '--type', '-t']);
|
|
1201
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1202
|
+
const arg = argv[i];
|
|
1203
|
+
if (flagsWithValues.has(arg)) {
|
|
1204
|
+
i++;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (!arg || arg.startsWith('-') || arg === 'help') continue;
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function normalizeComputerType(value) {
|
|
1214
|
+
const normalized = String(value || '')
|
|
1215
|
+
.trim()
|
|
1216
|
+
.toLowerCase()
|
|
1217
|
+
.replace(/[\s-]+/g, '_')
|
|
1218
|
+
.replace(/^_+|_+$/g, '');
|
|
1219
|
+
if (normalized === 'business') return 'business_ops';
|
|
1220
|
+
if (normalized === 'event') return 'event_ops';
|
|
1221
|
+
return normalized || 'general';
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function formatComputerTypeList() {
|
|
1225
|
+
return [...VALID_COMPUTER_TYPES].join(', ');
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function parseComputerDeleteArgs(argv = []) {
|
|
1229
|
+
const options = { help: false, confirm: null };
|
|
1230
|
+
|
|
1231
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1232
|
+
const arg = argv[i];
|
|
1233
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
1234
|
+
options.help = true;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (arg === '--confirm' && argv[i + 1]) {
|
|
1238
|
+
options.confirm = argv[i + 1];
|
|
1239
|
+
i++;
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
if (arg.startsWith('--confirm=')) {
|
|
1243
|
+
options.confirm = arg.slice('--confirm='.length);
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return options;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function formatCloudSelection(options = {}) {
|
|
1252
|
+
const worker = activeWorker(options.worker);
|
|
1253
|
+
const parts = [`worker=${worker}`];
|
|
1254
|
+
if (options.model) parts.push(`model=${options.model}`);
|
|
1255
|
+
if (!options.model) parts.push('model=default');
|
|
1256
|
+
return parts.join(' ');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function printModeBanner(mode, root, lines = []) {
|
|
1260
|
+
console.log(`Mode: ${mode}`);
|
|
1261
|
+
console.log(`Root: ${root}`);
|
|
1262
|
+
for (const line of lines) console.log(line);
|
|
1263
|
+
console.log('');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function findAtrisCodeTerminal() {
|
|
1267
|
+
const envPath = process.env.ATRIS_CODE_PY;
|
|
1268
|
+
const backend = resolveBackendRoot();
|
|
1269
|
+
const candidates = [
|
|
1270
|
+
envPath,
|
|
1271
|
+
path.join(__dirname, '..', 'cli', 'atris_code.py'),
|
|
1272
|
+
path.join(process.cwd(), 'cli', 'atris_code.py'),
|
|
1273
|
+
...(backend ? [path.join(backend, 'cli', 'atris_code.py')] : []),
|
|
1274
|
+
].filter(Boolean);
|
|
1275
|
+
|
|
1276
|
+
let dir = process.cwd();
|
|
1277
|
+
for (let i = 0; i < 4; i++) {
|
|
1278
|
+
candidates.push(path.join(dir, 'cli', 'atris_code.py'));
|
|
1279
|
+
dir = path.dirname(dir);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
for (const p of candidates) {
|
|
1283
|
+
if (fs.existsSync(p)) return p;
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function computerLocalLegacy(extraArgs = []) {
|
|
1289
|
+
printModeBanner('LOCAL', process.cwd(), [
|
|
1290
|
+
'Current folder is the workspace.',
|
|
1291
|
+
'Legacy console mode.',
|
|
1292
|
+
]);
|
|
1293
|
+
|
|
1294
|
+
const originalArgv = process.argv;
|
|
1295
|
+
process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], ...extraArgs];
|
|
1296
|
+
try {
|
|
1297
|
+
consoleCommand();
|
|
1298
|
+
} finally {
|
|
1299
|
+
process.argv = originalArgv;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function computerLocal(extraArgs = [], options = {}) {
|
|
1304
|
+
printLocalWordmark();
|
|
1305
|
+
printModeBanner('LOCAL', process.cwd(), [
|
|
1306
|
+
'Claude Code + Atris workspace context.',
|
|
1307
|
+
'BYO local Claude: tokens go through Anthropic, not Atris.',
|
|
1308
|
+
'No Atris credits, no cloud audit, no remote workspace.',
|
|
1309
|
+
'Remote /login only applies to Cloud workspace.',
|
|
1310
|
+
]);
|
|
1311
|
+
|
|
1312
|
+
const originalArgv = process.argv;
|
|
1313
|
+
process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], 'claude', ...extraArgs];
|
|
1314
|
+
try {
|
|
1315
|
+
consoleCommand(options);
|
|
1316
|
+
} finally {
|
|
1317
|
+
process.argv = originalArgv;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async function startLocalAtrisBridge(token, options = {}) {
|
|
1322
|
+
const workingDir = process.cwd();
|
|
1323
|
+
const allowBash = options.allowBash !== false;
|
|
1324
|
+
const result = await apiRequestJson('/cli/sessions', {
|
|
1325
|
+
method: 'POST',
|
|
1326
|
+
token,
|
|
1327
|
+
body: {
|
|
1328
|
+
working_directory: workingDir,
|
|
1329
|
+
agent_id: null,
|
|
1330
|
+
allow_bash: allowBash,
|
|
1331
|
+
},
|
|
1332
|
+
timeoutMs: 15000,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
if (!result.ok) {
|
|
1336
|
+
throw new Error(result.errorMessage || result.error || `failed to create local bridge (${result.status})`);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const session = result.data || {};
|
|
1340
|
+
const sessionId = session.session_id;
|
|
1341
|
+
if (!sessionId) {
|
|
1342
|
+
throw new Error('local bridge did not return a session id');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
let stopped = false;
|
|
1346
|
+
const loop = async () => {
|
|
1347
|
+
while (!stopped) {
|
|
1348
|
+
try {
|
|
1349
|
+
await streamSession(token, sessionId, workingDir);
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
if (!stopped) console.error(ui.dim(` local bridge reconnecting: ${err.message}`));
|
|
1352
|
+
}
|
|
1353
|
+
if (!stopped) await sleep(LOCAL_BRIDGE_RECONNECT_MS);
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
loop();
|
|
1357
|
+
|
|
1358
|
+
return {
|
|
1359
|
+
sessionId,
|
|
1360
|
+
workingDir,
|
|
1361
|
+
allowBash,
|
|
1362
|
+
stop: async () => {
|
|
1363
|
+
stopped = true;
|
|
1364
|
+
await apiRequestJson(`/cli/sessions/${sessionId}`, {
|
|
1365
|
+
method: 'DELETE',
|
|
1366
|
+
token,
|
|
1367
|
+
timeoutMs: 10000,
|
|
1368
|
+
}).catch(() => {});
|
|
1369
|
+
},
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async function runLocalBridgeOp(token, sessionId, op, timeoutSeconds = 30) {
|
|
1374
|
+
const result = await apiRequestJson(`/cli/sessions/${sessionId}/file-op`, {
|
|
1375
|
+
method: 'POST',
|
|
1376
|
+
token,
|
|
1377
|
+
body: {
|
|
1378
|
+
...op,
|
|
1379
|
+
wait_for_ack: true,
|
|
1380
|
+
timeout_seconds: timeoutSeconds,
|
|
1381
|
+
},
|
|
1382
|
+
timeoutMs: Math.max(10, timeoutSeconds + 5) * 1000,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
if (!result.ok) {
|
|
1386
|
+
console.error(`Failed: ${result.errorMessage || result.error || result.status}`);
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const data = result.data || {};
|
|
1391
|
+
if (data.status === 'error') {
|
|
1392
|
+
const err = data.result?.error || 'local operation failed';
|
|
1393
|
+
console.error(`Failed: ${err}`);
|
|
1394
|
+
}
|
|
1395
|
+
return data;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function readBusinessBinding(cwd = process.cwd()) {
|
|
1399
|
+
const bindingPath = path.join(cwd, '.atris', 'business.json');
|
|
1400
|
+
if (!fs.existsSync(bindingPath)) return null;
|
|
1401
|
+
try {
|
|
1402
|
+
return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
|
|
1403
|
+
} catch {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function readPackageMeta(cwd = process.cwd()) {
|
|
1409
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
1410
|
+
if (!fs.existsSync(packagePath)) return null;
|
|
1411
|
+
try {
|
|
1412
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
1413
|
+
} catch {
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function relIfExists(cwd, target) {
|
|
1419
|
+
return fs.existsSync(path.join(cwd, target)) ? target : null;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function detectValidationCommand(cwd = process.cwd(), pkg = null) {
|
|
1423
|
+
const meta = pkg || readPackageMeta(cwd);
|
|
1424
|
+
const testScript = meta?.scripts?.test;
|
|
1425
|
+
if (testScript && !/no test specified/i.test(testScript)) return 'npm test';
|
|
1426
|
+
if (fs.existsSync(path.join(cwd, 'pytest.ini')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) return 'pytest';
|
|
1427
|
+
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return 'cargo test';
|
|
1428
|
+
if (fs.existsSync(path.join(cwd, 'go.mod'))) return 'go test ./...';
|
|
1429
|
+
return 'none detected';
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function detectComputerType(cwd = process.cwd(), pkg = null, binding = null) {
|
|
1433
|
+
if (binding?.computer_type) return binding.computer_type;
|
|
1434
|
+
if (binding?.workspace_type) return binding.workspace_type;
|
|
1435
|
+
const meta = pkg || readPackageMeta(cwd);
|
|
1436
|
+
if (meta?.bin || fs.existsSync(path.join(cwd, 'bin')) || fs.existsSync(path.join(cwd, 'commands'))) return 'codeops';
|
|
1437
|
+
if (fs.existsSync(path.join(cwd, 'docs')) || fs.existsSync(path.join(cwd, 'atris', 'wiki'))) return 'research';
|
|
1438
|
+
return 'workspace';
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function buildComputerCard(cwd = process.cwd()) {
|
|
1442
|
+
const binding = readBusinessBinding(cwd);
|
|
1443
|
+
const pkg = readPackageMeta(cwd);
|
|
1444
|
+
const folderName = path.basename(cwd);
|
|
1445
|
+
const ownerName = binding?.name || pkg?.name || folderName;
|
|
1446
|
+
const ownerType = binding ? 'business' : 'project';
|
|
1447
|
+
const computerName = binding?.computer_name || binding?.workspace_name || `${ownerName} computer`;
|
|
1448
|
+
const computerType = detectComputerType(cwd, pkg, binding);
|
|
1449
|
+
const memory = [
|
|
1450
|
+
relIfExists(cwd, 'atris/MAP.md'),
|
|
1451
|
+
relIfExists(cwd, 'atris/TODO.md'),
|
|
1452
|
+
relIfExists(cwd, 'atris/wiki'),
|
|
1453
|
+
relIfExists(cwd, 'atris/logs'),
|
|
1454
|
+
].filter(Boolean);
|
|
1455
|
+
const artifacts = [
|
|
1456
|
+
fs.existsSync(path.join(cwd, 'atris')) ? 'atris/reports/' : null,
|
|
1457
|
+
relIfExists(cwd, '.atris/receipts'),
|
|
1458
|
+
].filter(Boolean);
|
|
1459
|
+
|
|
1460
|
+
return {
|
|
1461
|
+
ownerName,
|
|
1462
|
+
ownerType,
|
|
1463
|
+
computerName,
|
|
1464
|
+
computerType,
|
|
1465
|
+
workspace: cwd,
|
|
1466
|
+
loop: 'plan -> do -> review',
|
|
1467
|
+
memory,
|
|
1468
|
+
validation: detectValidationCommand(cwd, pkg),
|
|
1469
|
+
proof: binding ? 'atris computer proof' : 'atris proof run',
|
|
1470
|
+
visual: 'atris visualize "<prompt>"',
|
|
1471
|
+
artifacts,
|
|
1472
|
+
generatedAt: new Date().toISOString(),
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function renderList(items) {
|
|
1477
|
+
return items.length ? items.join(', ') : 'none detected';
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function renderComputerCard(card) {
|
|
1481
|
+
return [
|
|
1482
|
+
'Atris Computer Card',
|
|
1483
|
+
'',
|
|
1484
|
+
` Owner: ${card.ownerName} (${card.ownerType})`,
|
|
1485
|
+
` Computer: ${card.computerName}`,
|
|
1486
|
+
` Type: ${card.computerType}`,
|
|
1487
|
+
` Workspace: ${card.workspace}`,
|
|
1488
|
+
` Loop: ${card.loop}`,
|
|
1489
|
+
` Memory: ${renderList(card.memory)}`,
|
|
1490
|
+
` Validate: ${card.validation}`,
|
|
1491
|
+
` Proof: ${card.proof}`,
|
|
1492
|
+
` Visual: ${card.visual}`,
|
|
1493
|
+
` Artifacts: ${renderList(card.artifacts)}`,
|
|
1494
|
+
].join('\n');
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function renderComputerCardMarkdown(card) {
|
|
1498
|
+
return [
|
|
1499
|
+
'# Atris Computer Card',
|
|
1500
|
+
'',
|
|
1501
|
+
`Generated: ${card.generatedAt}`,
|
|
1502
|
+
'',
|
|
1503
|
+
`- Owner: ${card.ownerName} (${card.ownerType})`,
|
|
1504
|
+
`- Computer: ${card.computerName}`,
|
|
1505
|
+
`- Type: ${card.computerType}`,
|
|
1506
|
+
`- Workspace: ${card.workspace}`,
|
|
1507
|
+
`- Loop: ${card.loop}`,
|
|
1508
|
+
`- Memory: ${renderList(card.memory)}`,
|
|
1509
|
+
`- Validate: ${card.validation}`,
|
|
1510
|
+
`- Proof: ${card.proof}`,
|
|
1511
|
+
`- Visual: ${card.visual}`,
|
|
1512
|
+
`- Artifacts: ${renderList(card.artifacts)}`,
|
|
1513
|
+
'',
|
|
1514
|
+
].join('\n');
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function parseComputerCardArgs(args = []) {
|
|
1518
|
+
const options = { write: false, out: null, help: false };
|
|
1519
|
+
for (let i = 0; i < args.length; i++) {
|
|
1520
|
+
const arg = args[i];
|
|
1521
|
+
if (arg === '--help' || arg === '-h') options.help = true;
|
|
1522
|
+
else if (arg === '--write') options.write = true;
|
|
1523
|
+
else if (arg === '--out' && args[i + 1]) options.out = args[++i];
|
|
1524
|
+
else if (arg.startsWith('--out=')) options.out = arg.slice('--out='.length);
|
|
1525
|
+
}
|
|
1526
|
+
return options;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function defaultComputerCardPath(cwd = process.cwd()) {
|
|
1530
|
+
if (fs.existsSync(path.join(cwd, 'atris'))) {
|
|
1531
|
+
return path.join(cwd, 'atris', 'reports', 'computer-card.md');
|
|
1532
|
+
}
|
|
1533
|
+
return path.join(cwd, 'computer-card.md');
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function computerCard(args = [], cwd = process.cwd()) {
|
|
1537
|
+
const options = parseComputerCardArgs(args);
|
|
1538
|
+
if (options.help) {
|
|
1539
|
+
console.log('Usage: atris computer card [--write] [--out <path>]');
|
|
1540
|
+
console.log('');
|
|
1541
|
+
console.log('Show the local owner/computer card for this workspace.');
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const card = buildComputerCard(cwd);
|
|
1546
|
+
console.log(renderComputerCard(card));
|
|
1547
|
+
|
|
1548
|
+
if (options.write || options.out) {
|
|
1549
|
+
const outputPath = options.out
|
|
1550
|
+
? (path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out))
|
|
1551
|
+
: defaultComputerCardPath(cwd);
|
|
1552
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1553
|
+
fs.writeFileSync(outputPath, renderComputerCardMarkdown(card), 'utf8');
|
|
1554
|
+
console.log('');
|
|
1555
|
+
console.log(`Wrote ${path.relative(cwd, outputPath) || outputPath}`);
|
|
1556
|
+
return outputPath;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return card;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function savedBusinessWorker(slug) {
|
|
1563
|
+
if (!slug) return null;
|
|
1564
|
+
const wanted = String(slug).toLowerCase();
|
|
1565
|
+
const businesses = loadBusinesses();
|
|
1566
|
+
const entry = businesses[slug] || Object.values(businesses).find((item) => {
|
|
1567
|
+
if (!item) return false;
|
|
1568
|
+
return String(item.slug || '').toLowerCase() === wanted
|
|
1569
|
+
|| String(item.canonical_slug || '').toLowerCase() === wanted
|
|
1570
|
+
|| String(item.name || '').toLowerCase() === wanted;
|
|
1571
|
+
});
|
|
1572
|
+
return entry?.worker || null;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async function resolveBusinessContext(token) {
|
|
1576
|
+
const binding = readBusinessBinding();
|
|
1577
|
+
if (!binding) return null;
|
|
1578
|
+
|
|
1579
|
+
if (binding.business_id && binding.workspace_id) {
|
|
1580
|
+
return {
|
|
1581
|
+
slug: binding.slug,
|
|
1582
|
+
businessId: binding.business_id,
|
|
1583
|
+
workspaceId: binding.workspace_id,
|
|
1584
|
+
businessName: binding.name || binding.slug || 'business',
|
|
1585
|
+
worker: savedBusinessWorker(binding.slug),
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
const slug = binding.slug;
|
|
1590
|
+
if (!slug) return null;
|
|
1591
|
+
|
|
1592
|
+
const businesses = loadBusinesses();
|
|
1593
|
+
const list = await apiRequestJson('/business/', { method: 'GET', token });
|
|
1594
|
+
if (list.ok) {
|
|
1595
|
+
const match = (list.data || []).find(
|
|
1596
|
+
(b) => b.slug === slug || (b.name || '').toLowerCase() === slug.toLowerCase()
|
|
1597
|
+
);
|
|
1598
|
+
if (match) {
|
|
1599
|
+
businesses[slug] = {
|
|
1600
|
+
...(businesses[slug] || {}),
|
|
1601
|
+
business_id: match.id,
|
|
1602
|
+
workspace_id: match.workspace_id,
|
|
1603
|
+
name: match.name,
|
|
1604
|
+
slug: match.slug,
|
|
1605
|
+
added_at: new Date().toISOString(),
|
|
1606
|
+
};
|
|
1607
|
+
saveBusinesses(businesses);
|
|
1608
|
+
return {
|
|
1609
|
+
slug: match.slug,
|
|
1610
|
+
businessId: match.id,
|
|
1611
|
+
workspaceId: match.workspace_id,
|
|
1612
|
+
businessName: match.name || match.slug,
|
|
1613
|
+
worker: businesses[slug].worker || null,
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const cached = businesses[slug];
|
|
1619
|
+
if (cached && cached.business_id && cached.workspace_id) {
|
|
1620
|
+
return {
|
|
1621
|
+
slug,
|
|
1622
|
+
businessId: cached.business_id,
|
|
1623
|
+
workspaceId: cached.workspace_id,
|
|
1624
|
+
businessName: cached.name || slug,
|
|
1625
|
+
worker: cached.worker || null,
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function cachedBusinessContext(slug) {
|
|
1633
|
+
if (!slug) return null;
|
|
1634
|
+
const wanted = String(slug).toLowerCase();
|
|
1635
|
+
const businesses = loadBusinesses();
|
|
1636
|
+
const cached = businesses[slug] || Object.values(businesses).find((entry) => {
|
|
1637
|
+
if (!entry) return false;
|
|
1638
|
+
return String(entry.slug || '').toLowerCase() === wanted
|
|
1639
|
+
|| String(entry.canonical_slug || '').toLowerCase() === wanted
|
|
1640
|
+
|| String(entry.name || '').toLowerCase() === wanted;
|
|
1641
|
+
});
|
|
1642
|
+
if (!cached?.business_id) return null;
|
|
1643
|
+
return {
|
|
1644
|
+
slug: cached.slug || slug,
|
|
1645
|
+
businessId: cached.business_id,
|
|
1646
|
+
workspaceId: cached.workspace_id || null,
|
|
1647
|
+
businessName: cached.name || cached.slug || slug,
|
|
1648
|
+
worker: cached.worker || null,
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
async function resolveBusinessContextBySlug(token, slug, options = {}) {
|
|
1653
|
+
if (!slug) return null;
|
|
1654
|
+
|
|
1655
|
+
if (options.preferCache) {
|
|
1656
|
+
const cached = cachedBusinessContext(slug);
|
|
1657
|
+
if (cached?.workspaceId) return cached;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const businesses = loadBusinesses();
|
|
1661
|
+
const list = await apiRequestJson('/business/', { method: 'GET', token });
|
|
1662
|
+
if (list.ok) {
|
|
1663
|
+
const match = (list.data || []).find(
|
|
1664
|
+
(b) => b.slug === slug || (b.name || '').toLowerCase() === slug.toLowerCase()
|
|
1665
|
+
);
|
|
1666
|
+
if (match) {
|
|
1667
|
+
const key = match.slug || slug;
|
|
1668
|
+
businesses[key] = {
|
|
1669
|
+
...(businesses[key] || {}),
|
|
1670
|
+
business_id: match.id,
|
|
1671
|
+
workspace_id: match.workspace_id,
|
|
1672
|
+
name: match.name,
|
|
1673
|
+
slug: match.slug,
|
|
1674
|
+
added_at: new Date().toISOString(),
|
|
1675
|
+
};
|
|
1676
|
+
saveBusinesses(businesses);
|
|
1677
|
+
return {
|
|
1678
|
+
slug: match.slug,
|
|
1679
|
+
businessId: match.id,
|
|
1680
|
+
workspaceId: match.workspace_id,
|
|
1681
|
+
businessName: match.name || match.slug,
|
|
1682
|
+
worker: businesses[key].worker || null,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function shortcutBusinessLabel(slug, fallbackName) {
|
|
1691
|
+
if (normalizeBusinessSlug(slug) === RECRUITING_BUSINESS_SLUG) return 'Atris Labs';
|
|
1692
|
+
return fallbackName || slug;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
async function resolveSingleBusinessShortcutFallback(token, slug) {
|
|
1696
|
+
const wantedSlug = normalizeBusinessSlug(slug);
|
|
1697
|
+
if (wantedSlug !== RECRUITING_BUSINESS_SLUG) return null;
|
|
1698
|
+
|
|
1699
|
+
const list = await apiRequestJson('/business/', { method: 'GET', token });
|
|
1700
|
+
const rows = list.ok && Array.isArray(list.data)
|
|
1701
|
+
? list.data.filter((business) => business && business.id)
|
|
1702
|
+
: [];
|
|
1703
|
+
if (rows.length !== 1) return null;
|
|
1704
|
+
|
|
1705
|
+
const match = rows[0];
|
|
1706
|
+
const businesses = loadBusinesses();
|
|
1707
|
+
const businessName = shortcutBusinessLabel(wantedSlug, match.name || match.slug);
|
|
1708
|
+
businesses[wantedSlug] = {
|
|
1709
|
+
...(businesses[wantedSlug] || {}),
|
|
1710
|
+
business_id: match.id,
|
|
1711
|
+
workspace_id: match.workspace_id,
|
|
1712
|
+
name: businessName,
|
|
1713
|
+
slug: wantedSlug,
|
|
1714
|
+
cloud_slug: match.slug || null,
|
|
1715
|
+
added_at: new Date().toISOString(),
|
|
1716
|
+
};
|
|
1717
|
+
saveBusinesses(businesses);
|
|
1718
|
+
return {
|
|
1719
|
+
slug: wantedSlug,
|
|
1720
|
+
businessId: match.id,
|
|
1721
|
+
workspaceId: match.workspace_id,
|
|
1722
|
+
businessName,
|
|
1723
|
+
worker: businesses[wantedSlug].worker || null,
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
async function resolveComputerCommandContext(token, options = {}) {
|
|
1728
|
+
if (options.businessSlug || options.workspaceId) {
|
|
1729
|
+
const ctx = options.businessSlug
|
|
1730
|
+
? await resolveBusinessContextBySlug(token, options.businessSlug, { preferCache: true })
|
|
1731
|
+
: await resolveBusinessContext(token);
|
|
1732
|
+
if (!ctx?.businessId) return null;
|
|
1733
|
+
const workspaceId = options.workspaceId
|
|
1734
|
+
? await resolveWorkspaceSelector(token, ctx, options.workspaceId)
|
|
1735
|
+
: ctx.workspaceId;
|
|
1736
|
+
return {
|
|
1737
|
+
...ctx,
|
|
1738
|
+
workspaceId,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return resolveBusinessContext(token);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
async function resolveTypedBusinessComputerContext(token, options = {}, defaults = {}) {
|
|
1746
|
+
const businessSlug = options.businessSlug || defaults.businessSlug;
|
|
1747
|
+
const computerType = normalizeComputerType(defaults.computerType);
|
|
1748
|
+
if (options.workspaceId) {
|
|
1749
|
+
return resolveComputerCommandContext(token, { ...options, businessSlug });
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
let ctx = await resolveBusinessContextBySlug(token, businessSlug, { preferCache: true });
|
|
1753
|
+
if (!ctx?.businessId && (options.allowCanonicalShortcutFallback || defaults.allowCanonicalShortcutFallback)) {
|
|
1754
|
+
ctx = await resolveSingleBusinessShortcutFallback(token, businessSlug);
|
|
1755
|
+
}
|
|
1756
|
+
if (!ctx?.businessId) return null;
|
|
1757
|
+
const workspaces = await listBusinessWorkspaces(token, ctx);
|
|
1758
|
+
const workspace = resolveWorkspaceByComputerType(workspaces, computerType);
|
|
1759
|
+
if (!workspace?.id) {
|
|
1760
|
+
return {
|
|
1761
|
+
...ctx,
|
|
1762
|
+
workspaceId: null,
|
|
1763
|
+
missingComputerType: computerType,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
return {
|
|
1767
|
+
...ctx,
|
|
1768
|
+
workspaceId: workspace.id,
|
|
1769
|
+
workspaceName: workspace.name || null,
|
|
1770
|
+
computerType,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function printMissingTypedComputer(ctx, computerType, options = {}) {
|
|
1775
|
+
const label = options.label || computerType;
|
|
1776
|
+
const businessSlug = options.businessSlug || ctx?.slug || RECRUITING_BUSINESS_SLUG;
|
|
1777
|
+
console.error(`No ${label} computer found for ${ctx?.businessName || businessSlug}.`);
|
|
1778
|
+
console.error(`Create it: atris computer ${label} create`);
|
|
1779
|
+
console.error(`Or explicitly create one: atris computer create "${label[0].toUpperCase()}${label.slice(1)} Computer" --business ${businessSlug} --type ${computerType}`);
|
|
1780
|
+
process.exitCode = 1;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function looksLikeWorkspaceId(input) {
|
|
1784
|
+
const value = String(input || '').trim();
|
|
1785
|
+
if (!value) return false;
|
|
1786
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
1787
|
+
|| /^ws-[a-z0-9_-]+$/i.test(value);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
async function resolveWorkspaceSelector(token, ctx, input) {
|
|
1791
|
+
const selector = String(input || '').trim();
|
|
1792
|
+
if (!selector) return ctx.workspaceId;
|
|
1793
|
+
if (selector === ctx.workspaceId || looksLikeWorkspaceId(selector)) return selector;
|
|
1794
|
+
|
|
1795
|
+
const workspaces = await listBusinessWorkspaces(token, ctx);
|
|
1796
|
+
const workspace = resolveWorkspaceFromList(workspaces, selector);
|
|
1797
|
+
return workspace?.id || selector;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
async function resolveBusinessOwnerForCreate(token, businessSlug = null, options = {}) {
|
|
1801
|
+
const wantedSlug = businessSlug ? String(businessSlug).trim() : null;
|
|
1802
|
+
if (wantedSlug) {
|
|
1803
|
+
const fromApi = await resolveBusinessContextBySlug(token, wantedSlug);
|
|
1804
|
+
if (fromApi) return fromApi;
|
|
1805
|
+
|
|
1806
|
+
const cached = loadBusinesses()[wantedSlug];
|
|
1807
|
+
if (cached?.business_id) {
|
|
1808
|
+
return {
|
|
1809
|
+
slug: cached.slug || wantedSlug,
|
|
1810
|
+
businessId: cached.business_id,
|
|
1811
|
+
workspaceId: cached.workspace_id || null,
|
|
1812
|
+
businessName: cached.name || cached.slug || wantedSlug,
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
if (options.allowCanonicalShortcutFallback) {
|
|
1816
|
+
return resolveSingleBusinessShortcutFallback(token, wantedSlug);
|
|
1817
|
+
}
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const binding = readBusinessBinding();
|
|
1822
|
+
if (binding?.business_id) {
|
|
1823
|
+
return {
|
|
1824
|
+
slug: binding.slug || binding.canonical_slug || null,
|
|
1825
|
+
businessId: binding.business_id,
|
|
1826
|
+
workspaceId: binding.workspace_id || null,
|
|
1827
|
+
businessName: binding.name || binding.slug || 'business',
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return resolveBusinessContext(token);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function businessSelector(ctx) {
|
|
1835
|
+
return ctx?.slug || ctx?.businessId || '<business>';
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function apiFailureDetail(result) {
|
|
1839
|
+
return String(result?.errorMessage || result?.error || result?.data?.detail || result?.status || 'Request failed');
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function printComputerCommandFailure(result, ctx = null) {
|
|
1843
|
+
const detail = apiFailureDetail(result);
|
|
1844
|
+
console.error(detail);
|
|
1845
|
+
if (result?.status === 409) {
|
|
1846
|
+
const mismatch = extractAttachedWorkspaceMismatch(detail, result?.data);
|
|
1847
|
+
const targetWorkspace = mismatch?.requestedWorkspaceId || ctx?.workspaceId || '<workspace-id>';
|
|
1848
|
+
const forceFlag = /--force|force to take over|re-run with --force/i.test(detail) ? ' --force' : '';
|
|
1849
|
+
console.error(`Run: atris computer activate --business ${businessSelector(ctx)} --workspace ${targetWorkspace}${forceFlag}`);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function rememberBusinessWorkspace(ctx, workspaceId, options = {}) {
|
|
1854
|
+
const slug = ctx.slug || (ctx.businessName || '').toLowerCase().replace(/\s+/g, '-');
|
|
1855
|
+
if (!slug || !workspaceId) return;
|
|
1856
|
+
const businesses = loadBusinesses();
|
|
1857
|
+
const existing = businesses[slug] || {};
|
|
1858
|
+
businesses[slug] = {
|
|
1859
|
+
...existing,
|
|
1860
|
+
business_id: ctx.businessId,
|
|
1861
|
+
workspace_id: workspaceId,
|
|
1862
|
+
name: ctx.businessName,
|
|
1863
|
+
slug,
|
|
1864
|
+
computer_name: options.computerName || existing.computer_name,
|
|
1865
|
+
endpoint: options.endpoint || existing.endpoint,
|
|
1866
|
+
added_at: existing.added_at || new Date().toISOString(),
|
|
1867
|
+
updated_at: new Date().toISOString(),
|
|
1868
|
+
};
|
|
1869
|
+
saveBusinesses(businesses);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function rememberCreatedComputer(ctx, workspace, endpoint = null, options = {}) {
|
|
1873
|
+
const slug = ctx.slug || (ctx.businessName || '').toLowerCase().replace(/\s+/g, '-');
|
|
1874
|
+
if (!slug) return;
|
|
1875
|
+
const businesses = loadBusinesses();
|
|
1876
|
+
const existing = businesses[slug] || {};
|
|
1877
|
+
const shouldSetDefault = Boolean(options.setDefault || !existing.workspace_id);
|
|
1878
|
+
businesses[slug] = {
|
|
1879
|
+
...existing,
|
|
1880
|
+
business_id: ctx.businessId,
|
|
1881
|
+
workspace_id: shouldSetDefault ? workspace.id : existing.workspace_id,
|
|
1882
|
+
name: ctx.businessName,
|
|
1883
|
+
slug,
|
|
1884
|
+
computer_name: shouldSetDefault ? workspace.name : existing.computer_name,
|
|
1885
|
+
computer_type: shouldSetDefault ? (options.computerType || existing.computer_type) : existing.computer_type,
|
|
1886
|
+
endpoint: shouldSetDefault ? (endpoint || existing.endpoint) : existing.endpoint,
|
|
1887
|
+
added_at: existing.added_at || new Date().toISOString(),
|
|
1888
|
+
updated_at: new Date().toISOString(),
|
|
1889
|
+
};
|
|
1890
|
+
saveBusinesses(businesses);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
function shellQuote(value) {
|
|
1894
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function businessPromptUserId(token) {
|
|
1898
|
+
const claims = decodeJwtClaims(token) || {};
|
|
1899
|
+
return claims.sub || claims.user_id || claims.uid || null;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
async function runBusinessTerminalCommand(token, ctx, command, timeout = 30) {
|
|
1903
|
+
return apiRequestJson(
|
|
1904
|
+
`/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/terminal`,
|
|
1905
|
+
{
|
|
1906
|
+
method: 'POST',
|
|
1907
|
+
token,
|
|
1908
|
+
body: { command, timeout },
|
|
1909
|
+
timeoutMs: Math.max(timeout + 10, 40) * 1000,
|
|
1910
|
+
}
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
async function listBusinessWorkspaces(token, ctx) {
|
|
1915
|
+
const result = await apiRequestJson(`/business/${ctx.businessId}/workspaces`, {
|
|
1916
|
+
method: 'GET',
|
|
1917
|
+
token,
|
|
1918
|
+
timeoutMs: 15000,
|
|
1919
|
+
retries: 0,
|
|
1920
|
+
});
|
|
1921
|
+
return result.ok && Array.isArray(result.data) ? result.data : [];
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function formatWorkspaceRef(workspace) {
|
|
1925
|
+
if (!workspace) return '-';
|
|
1926
|
+
return workspace.name ? `${workspace.name} (${workspace.id})` : workspace.id;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function workspaceMatchesInput(workspace, input) {
|
|
1930
|
+
if (!workspace || !input) return false;
|
|
1931
|
+
const wanted = String(input).trim().toLowerCase();
|
|
1932
|
+
if (!wanted) return false;
|
|
1933
|
+
return String(workspace.id || '').toLowerCase() === wanted
|
|
1934
|
+
|| String(workspace.name || '').toLowerCase() === wanted;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function resolveWorkspaceFromList(workspaces, input) {
|
|
1938
|
+
return (workspaces || []).find((workspace) => workspaceMatchesInput(workspace, input)) || null;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function workspaceComputerType(workspace) {
|
|
1942
|
+
return normalizeComputerType(workspace?.type || workspace?.computer_type || workspace?.workspace_type || '');
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
function workspaceMatchesComputerType(workspace, type) {
|
|
1946
|
+
if (!workspace || !type) return false;
|
|
1947
|
+
const wanted = normalizeComputerType(type);
|
|
1948
|
+
if (workspaceComputerType(workspace) === wanted) return true;
|
|
1949
|
+
const compactWanted = wanted.replace(/_/g, '');
|
|
1950
|
+
const name = String(workspace.name || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
1951
|
+
return Boolean(compactWanted && name.includes(compactWanted));
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
function resolveWorkspaceByComputerType(workspaces, type) {
|
|
1955
|
+
return (workspaces || []).find((workspace) => workspaceMatchesComputerType(workspace, type)) || null;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function formatLeaseAge(seconds) {
|
|
1959
|
+
const value = Number(seconds);
|
|
1960
|
+
if (!Number.isFinite(value) || value < 0) return '-';
|
|
1961
|
+
if (value < 60) return `${Math.floor(value)}s`;
|
|
1962
|
+
const minutes = Math.floor(value / 60);
|
|
1963
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1964
|
+
const hours = Math.floor(minutes / 60);
|
|
1965
|
+
if (hours < 48) return `${hours}h`;
|
|
1966
|
+
return `${Math.floor(hours / 24)}d`;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async function probeAttachedWorkspace(token, ctx) {
|
|
1970
|
+
const result = await apiRequestJson(
|
|
1971
|
+
`/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/terminal`,
|
|
1972
|
+
{
|
|
1973
|
+
method: 'POST',
|
|
1974
|
+
token,
|
|
1975
|
+
body: { command: 'printf "ATRIS_STATUS_OK\\n"', timeout: 5 },
|
|
1976
|
+
timeoutMs: 8000,
|
|
1977
|
+
retries: 0,
|
|
1978
|
+
}
|
|
1979
|
+
);
|
|
1980
|
+
if (result.ok) return { workspaceId: ctx.workspaceId, health: 'ready' };
|
|
1981
|
+
const mismatch = extractAttachedWorkspaceMismatch(apiFailureDetail(result), result.data);
|
|
1982
|
+
if (mismatch?.attachedWorkspaceId) {
|
|
1983
|
+
return { workspaceId: mismatch.attachedWorkspaceId, health: 'workspace_mismatch', result };
|
|
1984
|
+
}
|
|
1985
|
+
return { workspaceId: null, health: 'degraded', result };
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer-wake', options = {}) {
|
|
1989
|
+
if (!ctx?.businessId || !ctx?.workspaceId) {
|
|
1990
|
+
return { ok: false, skipped: true, reason: 'missing_workspace' };
|
|
1991
|
+
}
|
|
1992
|
+
if (process.env.ATRIS_SKIP_RUNTIME_BOOTSTRAP === '1') {
|
|
1993
|
+
return { ok: true, skipped: true, reason: 'env' };
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
const command = buildRemoteAtrisBootstrapCommand({
|
|
1997
|
+
boundary,
|
|
1998
|
+
businessSlug: ctx.slug || '',
|
|
1999
|
+
businessId: ctx.businessId,
|
|
2000
|
+
workspaceId: ctx.workspaceId,
|
|
2001
|
+
});
|
|
2002
|
+
const result = await runBusinessTerminalCommand(token, ctx, command, 120);
|
|
2003
|
+
if (!result.ok) {
|
|
2004
|
+
if (!options.quiet) {
|
|
2005
|
+
console.log(' Runtime: bootstrap could not run.');
|
|
2006
|
+
console.log(` Recovery: atris computer run "npm install --prefix /workspace/.atris-npm atris@latest && /workspace/.atris-npm/node_modules/.bin/atris update" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
|
|
2007
|
+
}
|
|
2008
|
+
return { ok: false, result };
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const data = result.data || {};
|
|
2012
|
+
const output = String(data.stdout || data.output || data.result || '').trim();
|
|
2013
|
+
const line = output.split('\n').find((entry) => entry.includes('atris_runtime_bootstrap'));
|
|
2014
|
+
const recovery = output.split('\n').find((entry) => entry.startsWith('recovery='));
|
|
2015
|
+
if (!options.quiet) {
|
|
2016
|
+
if (line) {
|
|
2017
|
+
console.log(` Runtime: ${line.replace(/^atris_runtime_bootstrap\s*/, '')}`);
|
|
2018
|
+
} else {
|
|
2019
|
+
console.log(' Runtime: Atris bootstrap receipt written.');
|
|
2020
|
+
}
|
|
2021
|
+
if (recovery) {
|
|
2022
|
+
console.log(` Recovery: atris computer run "${recovery.slice('recovery='.length)}" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return { ok: true, output };
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
async function readBusinessWorkspaceFile(token, ctx, remotePath, timeoutMs = 15000) {
|
|
2029
|
+
return apiRequestJson(
|
|
2030
|
+
`/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/file?path=${encodeURIComponent(remotePath)}`,
|
|
2031
|
+
{
|
|
2032
|
+
method: 'GET',
|
|
2033
|
+
token,
|
|
2034
|
+
timeoutMs,
|
|
2035
|
+
}
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function extractRunnerProxyText(payload = {}) {
|
|
2040
|
+
const result = String(payload.result || '').trim();
|
|
2041
|
+
if (result) return result;
|
|
2042
|
+
if (Array.isArray(payload.assistant_text)) {
|
|
2043
|
+
const joined = payload.assistant_text.join('').trim();
|
|
2044
|
+
if (joined) return joined;
|
|
2045
|
+
}
|
|
2046
|
+
if (typeof payload.text === 'string' && payload.text.trim()) {
|
|
2047
|
+
return payload.text.trim();
|
|
2048
|
+
}
|
|
2049
|
+
return '';
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
async function runBusinessPromptViaRunnerProxy(token, ctx, prompt, options = {}) {
|
|
2053
|
+
const requestId = `cli-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2054
|
+
const remoteDir = '/workspace/.atris-runner-proxy';
|
|
2055
|
+
const outputPath = `${remoteDir}/${requestId}.json`;
|
|
2056
|
+
const scriptPath = `/tmp/atris_runner_proxy_${requestId}.py`;
|
|
2057
|
+
const stdoutPath = `/tmp/atris_runner_proxy_${requestId}.stdout`;
|
|
2058
|
+
const stderrPath = `/tmp/atris_runner_proxy_${requestId}.stderr`;
|
|
2059
|
+
const payload = {
|
|
2060
|
+
prompt,
|
|
2061
|
+
permission_mode: 'bypassPermissions',
|
|
2062
|
+
max_turns: Math.min(Math.max(Number(options.maxTurns || 40), 1), 100),
|
|
2063
|
+
reset_context: Boolean(options.resetContext),
|
|
2064
|
+
};
|
|
2065
|
+
if (options.worker) payload.worker = options.worker;
|
|
2066
|
+
if (options.model) payload.model = options.model;
|
|
2067
|
+
if (options.systemPrompt) payload.system_prompt = options.systemPrompt;
|
|
2068
|
+
if (options.allowedTools) payload.allowed_tools = options.allowedTools;
|
|
2069
|
+
if (options.localCliSessionId) payload.local_cli_session_id = options.localCliSessionId;
|
|
2070
|
+
|
|
2071
|
+
const userId = businessPromptUserId(token);
|
|
2072
|
+
if (userId) payload.user_id = userId;
|
|
2073
|
+
|
|
2074
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
|
|
2075
|
+
const remoteScript = [
|
|
2076
|
+
'import base64, json, pathlib, time, urllib.request',
|
|
2077
|
+
`PAYLOAD = json.loads(base64.b64decode(${JSON.stringify(payloadB64)}).decode("utf-8"))`,
|
|
2078
|
+
`OUTPUT_PATH = pathlib.Path(${JSON.stringify(outputPath)})`,
|
|
2079
|
+
'TOKEN = ""',
|
|
2080
|
+
'with open("/opt/atris/config/env", "r", encoding="utf-8") as fh:',
|
|
2081
|
+
' for line in fh:',
|
|
2082
|
+
' if line.startswith("ATRIS_SERVICE_TOKEN="):',
|
|
2083
|
+
' TOKEN = line.split("=", 1)[1].strip()',
|
|
2084
|
+
' break',
|
|
2085
|
+
'if not TOKEN:',
|
|
2086
|
+
' OUTPUT_PATH.write_text(json.dumps({"status":"error","error":"missing ATRIS_SERVICE_TOKEN"}), encoding="utf-8")',
|
|
2087
|
+
' raise SystemExit(0)',
|
|
2088
|
+
'def _fetch(req, timeout=120):',
|
|
2089
|
+
' with urllib.request.urlopen(req, timeout=timeout) as resp:',
|
|
2090
|
+
' return json.loads(resp.read().decode("utf-8"))',
|
|
2091
|
+
'try:',
|
|
2092
|
+
' start_req = urllib.request.Request(',
|
|
2093
|
+
' "http://127.0.0.1:8081/execute-background",',
|
|
2094
|
+
' data=json.dumps(PAYLOAD).encode("utf-8"),',
|
|
2095
|
+
' headers={"Content-Type":"application/json","X-Atris-Service-Token":TOKEN},',
|
|
2096
|
+
' method="POST",',
|
|
2097
|
+
' )',
|
|
2098
|
+
' start = _fetch(start_req)',
|
|
2099
|
+
' execution_id = start.get("execution_id")',
|
|
2100
|
+
' result = {"execution_id": execution_id, "assistant_text": [], "result": "", "status": "running", "result_event": None}',
|
|
2101
|
+
' from_index = 0',
|
|
2102
|
+
' deadline = time.time() + 300',
|
|
2103
|
+
' while time.time() < deadline:',
|
|
2104
|
+
' poll_req = urllib.request.Request(',
|
|
2105
|
+
' f"http://127.0.0.1:8081/events?execution_id={execution_id}&from_index={from_index}",',
|
|
2106
|
+
' headers={"X-Atris-Service-Token":TOKEN},',
|
|
2107
|
+
' method="GET",',
|
|
2108
|
+
' )',
|
|
2109
|
+
' data = _fetch(poll_req, timeout=60)',
|
|
2110
|
+
' events = data.get("events") or []',
|
|
2111
|
+
' for event in events:',
|
|
2112
|
+
' typ = event.get("type")',
|
|
2113
|
+
' if typ in ("assistant_text", "text"):',
|
|
2114
|
+
' content = event.get("content") or ""',
|
|
2115
|
+
' if content:',
|
|
2116
|
+
' result["assistant_text"].append(content)',
|
|
2117
|
+
' elif typ == "result":',
|
|
2118
|
+
' result["result"] = event.get("result") or result["result"]',
|
|
2119
|
+
' result["result_event"] = event',
|
|
2120
|
+
' from_index = data.get("next_index", from_index + len(events))',
|
|
2121
|
+
' result["status"] = data.get("status") or result["status"]',
|
|
2122
|
+
' if result["status"] in ("completed", "failed", "error", "cancelled"):',
|
|
2123
|
+
' break',
|
|
2124
|
+
' time.sleep(2)',
|
|
2125
|
+
' OUTPUT_PATH.write_text(json.dumps(result), encoding="utf-8")',
|
|
2126
|
+
'except Exception as exc:',
|
|
2127
|
+
' OUTPUT_PATH.write_text(json.dumps({"execution_id": None, "assistant_text": [], "result": "", "status": "error", "error": str(exc)}), encoding="utf-8")',
|
|
2128
|
+
].join('\n');
|
|
2129
|
+
|
|
2130
|
+
const launcher = [
|
|
2131
|
+
`mkdir -p ${shellQuote(remoteDir)}`,
|
|
2132
|
+
`cat > ${shellQuote(scriptPath)} <<'PY'`,
|
|
2133
|
+
remoteScript,
|
|
2134
|
+
'PY',
|
|
2135
|
+
`nohup python3 ${shellQuote(scriptPath)} >${shellQuote(stdoutPath)} 2>${shellQuote(stderrPath)} < /dev/null &`,
|
|
2136
|
+
'echo launched',
|
|
2137
|
+
].join('\n');
|
|
2138
|
+
|
|
2139
|
+
const launchResult = await runBusinessTerminalCommand(token, ctx, launcher, 30);
|
|
2140
|
+
if (!launchResult.ok) {
|
|
2141
|
+
return {
|
|
2142
|
+
ok: false,
|
|
2143
|
+
error: launchResult.error || `launcher failed (${launchResult.status})`,
|
|
2144
|
+
status: launchResult.status,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
const deadline = Date.now() + 330000;
|
|
2149
|
+
while (Date.now() < deadline) {
|
|
2150
|
+
const fileResult = await readBusinessWorkspaceFile(token, ctx, outputPath, 15000);
|
|
2151
|
+
if (!fileResult.ok) {
|
|
2152
|
+
if (fileResult.status === 404) {
|
|
2153
|
+
await sleep(2000);
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
return {
|
|
2157
|
+
ok: false,
|
|
2158
|
+
error: fileResult.error || `runner proxy read failed (${fileResult.status})`,
|
|
2159
|
+
status: fileResult.status,
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
try {
|
|
2163
|
+
const payload = JSON.parse(fileResult.data?.content || '{}');
|
|
2164
|
+
const status = payload.status || 'unknown';
|
|
2165
|
+
if (['completed', 'failed', 'error', 'cancelled', 'timeout'].includes(status)) {
|
|
2166
|
+
return { ok: status === 'completed', payload, status };
|
|
2167
|
+
}
|
|
2168
|
+
} catch {
|
|
2169
|
+
// Ignore partial file writes and keep polling.
|
|
2170
|
+
}
|
|
2171
|
+
await sleep(2000);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
return { ok: false, error: 'runner proxy timed out', status: 0 };
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
async function ensureBusinessAwake(token, ctx, maxWaitSec = 90, options = {}) {
|
|
2178
|
+
const status = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
|
|
2179
|
+
abortOnAuthFailure(status);
|
|
2180
|
+
if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) {
|
|
2181
|
+
return true;
|
|
2182
|
+
}
|
|
2183
|
+
if (!options.quiet) process.stdout.write(' Waking business computer... ');
|
|
2184
|
+
const wake = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, { method: 'POST', token, body: {} });
|
|
2185
|
+
abortOnAuthFailure(wake, !options.quiet);
|
|
2186
|
+
const start = Date.now();
|
|
2187
|
+
while (Date.now() - start < maxWaitSec * 1000) {
|
|
2188
|
+
await sleep(3000);
|
|
2189
|
+
const next = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
|
|
2190
|
+
abortOnAuthFailure(next, !options.quiet);
|
|
2191
|
+
if (next.ok && next.data && next.data.status === 'running' && next.data.endpoint) {
|
|
2192
|
+
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
2193
|
+
if (!options.quiet) console.log(`awake (${elapsed}s)`);
|
|
2194
|
+
await bootstrapBusinessComputerRuntime(token, ctx, 'computer-auto-wake', options);
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (!options.quiet) console.log('timeout');
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
async function cancelBusinessChat(token, ctx, executionId) {
|
|
2203
|
+
return apiRequestJson(
|
|
2204
|
+
`/business/${ctx.businessId}/chat/cancel?execution_id=${encodeURIComponent(executionId)}&workspace_id=${encodeURIComponent(ctx.workspaceId)}`,
|
|
2205
|
+
{ method: 'POST', token, timeoutMs: 15000, retries: 0 }
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
async function fetchBusinessChatAudit(token, ctx, limit = 10) {
|
|
2210
|
+
return apiRequestJson(
|
|
2211
|
+
`/business/${ctx.businessId}/chat/audit?workspace_id=${encodeURIComponent(ctx.workspaceId)}&limit=${Math.max(1, Math.min(limit, 25))}`,
|
|
2212
|
+
{ method: 'GET', token, timeoutMs: 15000, retries: 0 }
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
async function startBusinessClaudeLogin(token, ctx) {
|
|
2217
|
+
return apiRequestJson('/sandbox-secrets/claude-login/start', {
|
|
2218
|
+
method: 'POST',
|
|
2219
|
+
token,
|
|
2220
|
+
body: { business_id: ctx.businessId },
|
|
2221
|
+
timeoutMs: 15000,
|
|
2222
|
+
retries: 0,
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
async function fetchBusinessClaudeLoginStatus(token, ctx) {
|
|
2227
|
+
return apiRequestJson(
|
|
2228
|
+
`/sandbox-secrets/claude-login/status?business_id=${encodeURIComponent(ctx.businessId)}`,
|
|
2229
|
+
{ method: 'GET', token, timeoutMs: 15000, retries: 0 }
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
async function submitBusinessClaudeLoginCode(token, ctx, code) {
|
|
2234
|
+
return apiRequestJson('/sandbox-secrets/claude-login/input', {
|
|
2235
|
+
method: 'POST',
|
|
2236
|
+
token,
|
|
2237
|
+
body: { business_id: ctx.businessId, code },
|
|
2238
|
+
timeoutMs: 15000,
|
|
2239
|
+
retries: 0,
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
async function stopBusinessClaudeLogin(token, ctx) {
|
|
2244
|
+
return apiRequestJson('/sandbox-secrets/claude-login/stop', {
|
|
2245
|
+
method: 'POST',
|
|
2246
|
+
token,
|
|
2247
|
+
body: { business_id: ctx.businessId },
|
|
2248
|
+
timeoutMs: 15000,
|
|
2249
|
+
retries: 0,
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
function maybeOpenUrl(url) {
|
|
2254
|
+
if (!url) return;
|
|
2255
|
+
if (process.platform === 'darwin') {
|
|
2256
|
+
spawnSync('open', [url], { stdio: 'ignore' });
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
async function computerCloudLogin(token, ctx, rawArg = '') {
|
|
2261
|
+
if (!ctx) {
|
|
2262
|
+
console.error('Cloud login requires a bound business workspace.');
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const arg = String(rawArg || '').trim();
|
|
2267
|
+
if (arg.toLowerCase() === 'stop') {
|
|
2268
|
+
const stopped = await stopBusinessClaudeLogin(token, ctx);
|
|
2269
|
+
if (!stopped.ok) {
|
|
2270
|
+
console.error(`Failed: ${stopped.errorMessage || stopped.status}`);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
console.log('Claude login stopped.');
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
if (arg) {
|
|
2278
|
+
const submitted = await submitBusinessClaudeLoginCode(token, ctx, arg);
|
|
2279
|
+
if (!submitted.ok) {
|
|
2280
|
+
console.error(`Failed: ${submitted.errorMessage || submitted.status}`);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
console.log(`Claude login status: ${submitted.data?.status || 'running'}`);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
const started = await startBusinessClaudeLogin(token, ctx);
|
|
2288
|
+
if (!started.ok) {
|
|
2289
|
+
console.error(`Failed: ${started.errorMessage || started.status}`);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
let state = started.data || {};
|
|
2294
|
+
const startedAt = Date.now();
|
|
2295
|
+
while (!state.url && !['completed', 'failed', 'idle'].includes(state.status || '') && Date.now() - startedAt < 15000) {
|
|
2296
|
+
await sleep(1000);
|
|
2297
|
+
const status = await fetchBusinessClaudeLoginStatus(token, ctx);
|
|
2298
|
+
if (!status.ok) {
|
|
2299
|
+
console.error(`Failed: ${status.errorMessage || status.status}`);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
state = status.data || {};
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
if (state.loggedIn || state.status === 'completed') {
|
|
2306
|
+
console.log('Claude App is already logged in on this computer.');
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
if (state.url) {
|
|
2310
|
+
console.log('Open this URL to log the remote computer into your Claude subscription:');
|
|
2311
|
+
console.log(state.url);
|
|
2312
|
+
maybeOpenUrl(state.url);
|
|
2313
|
+
console.log('After approval, paste the code with `/login <code>`.');
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (state.output) {
|
|
2317
|
+
console.log(state.output);
|
|
2318
|
+
console.log('If Claude asks for a code, paste it with `/login <code>`.');
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
console.log(`Claude login status: ${state.status || 'unknown'}`);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function printBusinessChatAudit(rows) {
|
|
2325
|
+
console.log('');
|
|
2326
|
+
console.log(ui.bold('Recent cloud runs'));
|
|
2327
|
+
if (!rows.length) {
|
|
2328
|
+
console.log(' No recent cloud runs.');
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
for (const row of rows) {
|
|
2332
|
+
const when = row.started_at ? String(row.started_at).replace('T', ' ').replace(/\.\d+/, '').replace('+00:00', 'Z') : '-';
|
|
2333
|
+
const tokens = Number.isFinite(row.tokens_used) ? `${row.tokens_used} tok` : '-';
|
|
2334
|
+
const cost = Number.isFinite(row.cost_usd) ? `$${Number(row.cost_usd).toFixed(4)}` : '-';
|
|
2335
|
+
const credits = Number(row.credits_charged || 0);
|
|
2336
|
+
const charge = credits > 0 ? ui.yellow(`${credits} cr`) : ui.green('0 cr');
|
|
2337
|
+
console.log(` ${when} ${row.status || 'unknown'} ${tokens} cost ${cost} charge ${charge}`);
|
|
2338
|
+
if (row.worker || row.model) console.log(` ${row.worker || '-'} ${row.model || '-'}`);
|
|
2339
|
+
if (row.preview) console.log(` ${String(row.preview).slice(0, 140)}`);
|
|
2340
|
+
if (row.result_preview) console.log(` => ${String(row.result_preview).slice(0, 180)}`);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
async function computerAudit(token, ctx, limit = 10) {
|
|
2345
|
+
if (!ctx) {
|
|
2346
|
+
console.error('Cloud audit requires a bound business workspace.');
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const result = await fetchBusinessChatAudit(token, ctx, limit);
|
|
2350
|
+
if (!result.ok) {
|
|
2351
|
+
console.error(`Failed: ${result.errorMessage || result.status}`);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
printBusinessChatAudit(result.data?.rows || []);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
module.exports = {
|
|
2358
|
+
VALID_CLOUD_WORKERS,
|
|
2359
|
+
VALID_COMPUTER_TYPES,
|
|
2360
|
+
RECRUITING_BUSINESS_SLUG,
|
|
2361
|
+
RECRUITING_LOCAL_SYNC_COMMANDS,
|
|
2362
|
+
KNOWN_CHAT_COMMANDS,
|
|
2363
|
+
CODEOPS_WORKFLOW_PROMPT,
|
|
2364
|
+
RECRUITING_WORKFLOW_PROMPT,
|
|
2365
|
+
sleep,
|
|
2366
|
+
askComputerSetup,
|
|
2367
|
+
setupComputerChoices,
|
|
2368
|
+
chooseSetupComputer,
|
|
2369
|
+
chooseSetupProviders,
|
|
2370
|
+
ui,
|
|
2371
|
+
useInteractiveCloudUi,
|
|
2372
|
+
useInteractiveTerminalUi,
|
|
2373
|
+
readPipedStdin,
|
|
2374
|
+
printCloudWordmark,
|
|
2375
|
+
printLocalWordmark,
|
|
2376
|
+
activeWorker,
|
|
2377
|
+
formatWorkerName,
|
|
2378
|
+
extractAttachedWorkspaceMismatch,
|
|
2379
|
+
contextForAttachedWorkspaceMismatch,
|
|
2380
|
+
describeClaudeAuth,
|
|
2381
|
+
describeBillingMode,
|
|
2382
|
+
printCloudHelp,
|
|
2383
|
+
printCloudStartPanel,
|
|
2384
|
+
appendSystemPrompt,
|
|
2385
|
+
codeOpsCloudOptions,
|
|
2386
|
+
recruitingCloudOptions,
|
|
2387
|
+
printCodeOpsStartPanel,
|
|
2388
|
+
printCodeOpsWorkflowContract,
|
|
2389
|
+
printRecruitingWorkflowContract,
|
|
2390
|
+
printRecruitingComputerHelp,
|
|
2391
|
+
withoutRecruitingWrapperFlags,
|
|
2392
|
+
printRecruitingLocalSyncOutcome,
|
|
2393
|
+
runRecruitingLocalSyncCommand,
|
|
2394
|
+
runRecruitingSyncHelper,
|
|
2395
|
+
buildLocalBridgeSystemPrompt,
|
|
2396
|
+
printLocalAtrisStartPanel,
|
|
2397
|
+
printCloudSessionStatus,
|
|
2398
|
+
questionAsync,
|
|
2399
|
+
chooseComputerSurface,
|
|
2400
|
+
chooseCloudLane,
|
|
2401
|
+
parseComputerOptions,
|
|
2402
|
+
parseComputerCreateArgs,
|
|
2403
|
+
computerCreateArgsHaveName,
|
|
2404
|
+
normalizeComputerType,
|
|
2405
|
+
formatComputerTypeList,
|
|
2406
|
+
parseComputerDeleteArgs,
|
|
2407
|
+
formatCloudSelection,
|
|
2408
|
+
findAtrisCodeTerminal,
|
|
2409
|
+
computerLocalLegacy,
|
|
2410
|
+
computerLocal,
|
|
2411
|
+
startLocalAtrisBridge,
|
|
2412
|
+
runLocalBridgeOp,
|
|
2413
|
+
readBusinessBinding,
|
|
2414
|
+
buildComputerCard,
|
|
2415
|
+
renderComputerCard,
|
|
2416
|
+
renderComputerCardMarkdown,
|
|
2417
|
+
parseComputerCardArgs,
|
|
2418
|
+
computerCard,
|
|
2419
|
+
resolveBusinessContext,
|
|
2420
|
+
resolveBusinessContextBySlug,
|
|
2421
|
+
resolveComputerCommandContext,
|
|
2422
|
+
resolveTypedBusinessComputerContext,
|
|
2423
|
+
printMissingTypedComputer,
|
|
2424
|
+
looksLikeWorkspaceId,
|
|
2425
|
+
resolveBusinessOwnerForCreate,
|
|
2426
|
+
apiFailureDetail,
|
|
2427
|
+
printComputerCommandFailure,
|
|
2428
|
+
rememberBusinessWorkspace,
|
|
2429
|
+
rememberCreatedComputer,
|
|
2430
|
+
shellQuote,
|
|
2431
|
+
listBusinessWorkspaces,
|
|
2432
|
+
formatWorkspaceRef,
|
|
2433
|
+
workspaceMatchesInput,
|
|
2434
|
+
resolveWorkspaceFromList,
|
|
2435
|
+
workspaceMatchesComputerType,
|
|
2436
|
+
formatLeaseAge,
|
|
2437
|
+
probeAttachedWorkspace,
|
|
2438
|
+
bootstrapBusinessComputerRuntime,
|
|
2439
|
+
readBusinessWorkspaceFile,
|
|
2440
|
+
extractRunnerProxyText,
|
|
2441
|
+
runBusinessPromptViaRunnerProxy,
|
|
2442
|
+
ensureBusinessAwake,
|
|
2443
|
+
cancelBusinessChat,
|
|
2444
|
+
fetchBusinessChatAudit,
|
|
2445
|
+
computerCloudLogin,
|
|
2446
|
+
computerAudit,
|
|
2447
|
+
};
|