-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.ts
More file actions
175 lines (156 loc) · 6.19 KB
/
Copy pathrun.ts
File metadata and controls
175 lines (156 loc) · 6.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import Anthropic from '@anthropic-ai/sdk';
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import * as readline from 'readline/promises';
import { stdin as input, stdout as output } from 'process';
const AGENT_ID = process.env.AGENT_ID;
const ENV_ID = process.env.ENV_ID;
const RESUME_SESSION_ID = process.env.SESSION_ID;
const START_PHASE = parseInt(process.env.START_PHASE || '1', 10); // 1-indexed
if (!RESUME_SESSION_ID && (!AGENT_ID || !ENV_ID)) {
console.error('Set AGENT_ID and ENV_ID (or SESSION_ID to resume).');
process.exit(1);
}
const INTERACTIVE = process.stdin.isTTY === true;
const OUTPUTS_DIR = path.resolve('./outputs');
fs.mkdirSync(OUTPUTS_DIR, { recursive: true });
const client = new Anthropic();
const PHASES: Array<{ label: string; prompt: string }> = [
{
label: 'Phase 1 — districts',
prompt:
'Phase 1: Research and assemble the 30 largest school districts in Oregon by student enrollment. ' +
'For each, capture: name, HQ city, county, enrollment count, and homepage URL. ' +
'Save to /mnt/session/outputs/districts.json and generate /mnt/session/outputs/dashboard.html ' +
'showing district locations on the Leaflet map. Stop when done.',
},
{
label: 'Phase 2 — software detection',
prompt:
'Phase 2: For each district from districts.json, visit the homepage and any linked IT, parent, ' +
'or student portal pages. Detect mentions of: PowerSchool, Infinite Campus, Synergy, ParentVUE, ' +
'StudentVUE, Canvas, Google Classroom, Schoology, Seesaw, ClassDojo, Skyward. Update ' +
'districts.json (add a `software` array per district with {name, source_url}) and regenerate ' +
'dashboard.html so popups show detected software. Stop when done.',
},
{
label: 'Phase 3 — standards detection',
prompt:
'Phase 3: Call the EDUcore MCP tools to fetch the canonical list of interoperability and content ' +
'standards. For each district, search the website (and any tech/IT pages) for mentions of those ' +
'standards. Update districts.json (add a `standards` array per district with {name, source_url}) ' +
'and regenerate dashboard.html so popups show detected standards. Stop when done.',
},
];
async function ask(rl: readline.Interface | null, prompt: string): Promise<string> {
if (!rl) {
process.stdout.write(prompt + ' [auto-yes]\n');
return 'y';
}
try {
return await rl.question(prompt);
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code === 'ERR_USE_AFTER_CLOSE') return 'y';
throw err;
}
}
async function main() {
let sessionId: string;
if (RESUME_SESSION_ID) {
const s = await client.beta.sessions.retrieve(RESUME_SESSION_ID);
if (s.archived_at) {
console.error(`Session ${s.id} is archived. Cannot resume.`);
process.exit(1);
}
sessionId = s.id;
console.log(`\nResuming session: ${sessionId} (status: ${s.status}, starting at phase ${START_PHASE})\n`);
} else {
const session = await client.beta.sessions.create({
agent: AGENT_ID!,
environment_id: ENV_ID!,
title: 'Oregon EdTech Standards — phased run',
});
sessionId = session.id;
console.log(`\nSession: ${sessionId}\n`);
}
const rl = INTERACTIVE ? readline.createInterface({ input, output }) : null;
for (let i = START_PHASE - 1; i < PHASES.length; i++) {
const phase = PHASES[i];
const ans = (await ask(rl, `\n>>> Run ${phase.label}? [Y/n] `)).trim().toLowerCase();
if (ans === 'n') break;
console.log(`\n--- ${phase.label} ---\n`);
await runPhase(sessionId, phase.prompt);
await downloadAndOpen(sessionId);
}
const archAns = (await ask(rl, '\nArchive session? [Y/n] ')).trim().toLowerCase();
if (rl) rl.close();
if (archAns !== 'n') {
// Brief poll to dodge the post-idle status-write race
for (let i = 0; i < 10; i++) {
const s = await client.beta.sessions.retrieve(sessionId);
if (s.status !== 'running') break;
await new Promise((r) => setTimeout(r, 200));
}
await client.beta.sessions.archive(sessionId);
console.log('Archived.');
} else {
console.log(`Session left open: ${sessionId}`);
}
}
async function runPhase(sessionId: string, message: string) {
// Stream-first: open stream before sending so we don't miss early events
const stream = await client.beta.sessions.events.stream(sessionId);
await client.beta.sessions.events.send(sessionId, {
events: [{ type: 'user.message', content: [{ type: 'text', text: message }] }],
});
for await (const event of stream) {
switch (event.type) {
case 'agent.message':
for (const block of event.content) {
if (block.type === 'text') process.stdout.write(block.text);
}
break;
case 'agent.tool_use':
process.stdout.write(`\n [tool: ${event.name}]\n`);
break;
case 'agent.mcp_tool_use':
process.stdout.write(`\n [mcp: ${event.name}]\n`);
break;
case 'session.error':
console.error('\n[session.error]', JSON.stringify(event, null, 2));
break;
case 'session.status_terminated':
console.log('\n[session terminated]');
return;
case 'session.status_idle':
// Don't break on bare idle — could be transient (awaiting a tool result, etc.)
if (event.stop_reason.type === 'requires_action') continue;
return;
}
}
}
async function downloadAndOpen(sessionId: string) {
// Brief indexing lag between idle and files appearing
await new Promise((r) => setTimeout(r, 2500));
const files = await client.beta.files.list({
scope_id: sessionId,
betas: ['managed-agents-2026-04-01'],
});
for (const f of files.data) {
const safeName = path.basename(f.filename);
const localPath = path.join(OUTPUTS_DIR, safeName);
const resp = await client.beta.files.download(f.id);
const buf = Buffer.from(await resp.arrayBuffer());
fs.writeFileSync(localPath, buf);
console.log(` → ${localPath} (${f.size_bytes} bytes)`);
}
const dashboard = path.join(OUTPUTS_DIR, 'dashboard.html');
if (fs.existsSync(dashboard)) {
exec(`open "${dashboard}"`);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});