@bill10/agent-007 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/VERSION +1 -0
- package/bin/adduser.js +69 -0
- package/bin/agent-007.js +88 -0
- package/lib/cron.js +189 -0
- package/lib/helpers.js +541 -0
- package/lib/jobs.js +965 -0
- package/package.json +63 -0
- package/public/app.js +650 -0
- package/public/assets/characters/LICENSE +21 -0
- package/public/assets/characters/char_0.png +0 -0
- package/public/assets/characters/char_1.png +0 -0
- package/public/assets/characters/char_2.png +0 -0
- package/public/assets/characters/char_3.png +0 -0
- package/public/assets/characters/char_4.png +0 -0
- package/public/assets/characters/char_5.png +0 -0
- package/public/assets/furniture/bookshelf.png +0 -0
- package/public/assets/furniture/cactus.png +0 -0
- package/public/assets/furniture/chair_back.png +0 -0
- package/public/assets/furniture/chair_front.png +0 -0
- package/public/assets/furniture/chair_side.png +0 -0
- package/public/assets/furniture/coffee.png +0 -0
- package/public/assets/furniture/coffee_table.png +0 -0
- package/public/assets/furniture/desk.png +0 -0
- package/public/assets/furniture/desk2.png +0 -0
- package/public/assets/furniture/plant_2.png +0 -0
- package/public/assets/furniture/sofa_front.png +0 -0
- package/public/assets/furniture/sofa_side.png +0 -0
- package/public/assets/furniture/table_front.png +0 -0
- package/public/index.html +245 -0
- package/public/modules/auth.js +83 -0
- package/public/modules/explorer.js +760 -0
- package/public/modules/jobs.js +971 -0
- package/public/modules/office.js +2154 -0
- package/public/modules/paths.js +20 -0
- package/public/modules/shortcuts.js +54 -0
- package/public/modules/state.js +75 -0
- package/public/modules/terminal.js +651 -0
- package/public/modules/voice.js +393 -0
- package/public/modules/ws.js +56 -0
- package/public/style.css +1843 -0
- package/server/agent-mcp-bridge.js +45 -0
- package/server/agent-mcp.js +184 -0
- package/server/agent-transcripts.js +195 -0
- package/server/approvals.js +155 -0
- package/server/auth.js +162 -0
- package/server/billion.js +176 -0
- package/server/claude-trust.js +66 -0
- package/server/command-path.js +102 -0
- package/server/config.js +184 -0
- package/server/direct-run.js +33 -0
- package/server/git.js +630 -0
- package/server/http.js +276 -0
- package/server/jobs.js +2044 -0
- package/server/mcp.js +596 -0
- package/server/messages.js +319 -0
- package/server/permission-hook.js +47 -0
- package/server/pty.js +360 -0
- package/server/state.js +104 -0
- package/server/ws.js +583 -0
- package/server.js +306 -0
- package/templates/billion/COMPANY.md +14 -0
- package/templates/billion/STATE.md +17 -0
- package/templates/billion/charter.md +232 -0
- package/templates/billion/owner.md +11 -0
package/server/config.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Config persistence — load, save, orphan tracking, crash recovery
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { basename } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
config, setConfig, orphans, codenamePool,
|
|
7
|
+
CONFIG_DIR, CONFIG_PATH,
|
|
8
|
+
} from './state.js';
|
|
9
|
+
import { nextCronIso } from '../lib/cron.js';
|
|
10
|
+
import { isScheduled, jobRequiresPr, sessionAgentFromCommand, isValidJobAgent, permissionFlagsFromCommand, recordedPermissionFlags } from '../lib/jobs.js';
|
|
11
|
+
|
|
12
|
+
export function loadConfig() {
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
15
|
+
setConfig({ version: 1, repos: [], orphans: [], activeSessions: [], jobs: [], jobBoard: null });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const data = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
19
|
+
setConfig(data);
|
|
20
|
+
if (!config.version) config.version = 1;
|
|
21
|
+
if (!Array.isArray(config.repos)) config.repos = [];
|
|
22
|
+
if (!Array.isArray(config.orphans)) config.orphans = [];
|
|
23
|
+
if (!Array.isArray(config.activeSessions)) config.activeSessions = [];
|
|
24
|
+
if (!Array.isArray(config.jobs)) config.jobs = [];
|
|
25
|
+
// Jobs outlive the server, sessions do not. Every in-progress job now has an
|
|
26
|
+
// agentSessionId pointing at a PTY that no longer exists, so the link goes.
|
|
27
|
+
// What happens next depends on whether the job got as far as a branch:
|
|
28
|
+
//
|
|
29
|
+
// - No branch: nothing was ever started, so send it back to To do and let
|
|
30
|
+
// the dispatcher pick it up.
|
|
31
|
+
// - Has a branch: the agent may already have pushed and opened the PR. It
|
|
32
|
+
// STAYS in-progress so checkPullRequests can find that PR and move it to
|
|
33
|
+
// Review. Sending it back to To do would dispatch a second agent onto a
|
|
34
|
+
// `-2` branch to redo work that is already up for review, and open a
|
|
35
|
+
// duplicate PR. It does not block the queue: the cap ignores jobs whose
|
|
36
|
+
// agent is not live, so a null session link keeps the slot free. If
|
|
37
|
+
// there turns out to be no PR, the card shows "agent gone" and the user
|
|
38
|
+
// decides — requeue, or recover the branch from the orphans list.
|
|
39
|
+
for (const job of config.jobs) {
|
|
40
|
+
// No session survives a restart, so no stored link can still be valid —
|
|
41
|
+
// and ids are only unique within a process generation, so a stale one can
|
|
42
|
+
// otherwise resolve to an unrelated agent. Cleared for EVERY job, not
|
|
43
|
+
// just the in-flight ones: a review card kept its link forever.
|
|
44
|
+
job.agentSessionId = null;
|
|
45
|
+
// A schedule is never dispatched, but one saved mid-run by a server from
|
|
46
|
+
// before v0.4.9.0 (when it was) goes back to To do, as that server's own
|
|
47
|
+
// restart did. Its agent is dead either way; the note names its branch.
|
|
48
|
+
if (isScheduled(job)) {
|
|
49
|
+
delete job.lastRunSessionId;
|
|
50
|
+
delete job.lastRunAgentName;
|
|
51
|
+
}
|
|
52
|
+
if (isScheduled(job) && job.state !== 'todo') {
|
|
53
|
+
if (job.branchName) {
|
|
54
|
+
job.lastError = `Server restarted mid-run — that run's work is on ${job.branchName} (recover the worktree from the orphans list if you need it).`;
|
|
55
|
+
job.lastErrorAt = new Date().toISOString();
|
|
56
|
+
}
|
|
57
|
+
// Re-armed from now, as the old restart did, so the run a restart cut
|
|
58
|
+
// short does not go straight out again.
|
|
59
|
+
Object.assign(job, {
|
|
60
|
+
state: 'todo', agentName: null, startedAt: null, branchName: null, worktreePath: null,
|
|
61
|
+
nextRunAt: job.schedule ? nextCronIso(job.schedule) : null,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (job.state !== 'in-progress') continue;
|
|
66
|
+
// agentName is history, not a live link — "Phantom did this work" stays
|
|
67
|
+
// true across a restart, and it is the credit the card exists to show.
|
|
68
|
+
if (!job.branchName) {
|
|
69
|
+
job.state = 'todo';
|
|
70
|
+
job.agentName = null;
|
|
71
|
+
job.startedAt = null;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// A card that needs no PR has nothing for the board to watch: only its
|
|
75
|
+
// agent can finish it, so the note points at re-adopting it instead.
|
|
76
|
+
job.lastError = jobRequiresPr(job)
|
|
77
|
+
? `Server restarted — agent lost. Work is on ${job.branchName}; the board is still watching for its PR (recover the worktree from the orphans list if it never opened one).`
|
|
78
|
+
: `Server restarted — agent lost. Work is on ${job.branchName}; re-adopt it from the orphans list so it can finish, or move this card by hand.`;
|
|
79
|
+
job.lastErrorAt = new Date().toISOString();
|
|
80
|
+
}
|
|
81
|
+
for (const o of config.orphans) {
|
|
82
|
+
orphans.set(o.id, o);
|
|
83
|
+
codenamePool.addUsed(o.name);
|
|
84
|
+
if (o.worktreePath) codenamePool.addUsed(basename(o.worktreePath)); // the label may have been renamed
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn('Config corrupted, starting with empty config:', err.message);
|
|
88
|
+
setConfig({ version: 1, repos: [], orphans: [], activeSessions: [], jobs: [], jobBoard: null });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function saveConfig(broadcast) {
|
|
93
|
+
try {
|
|
94
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
95
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('Failed to save config:', err.message);
|
|
98
|
+
if (broadcast) broadcast({ type: 'notification', level: 'error', message: 'Failed to save config: ' + err.message });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function syncOrphansToConfig(broadcast) {
|
|
103
|
+
config.orphans = [...orphans.values()];
|
|
104
|
+
saveConfig(broadcast);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function saveActiveSession(session, broadcast) {
|
|
108
|
+
if (!session.worktreePath) return;
|
|
109
|
+
config.activeSessions.push({
|
|
110
|
+
name: session.name,
|
|
111
|
+
repoPath: session.repoPath,
|
|
112
|
+
repoSlug: session.repoSlug,
|
|
113
|
+
worktreePath: session.worktreePath,
|
|
114
|
+
branchName: session.branchName,
|
|
115
|
+
color: session.color,
|
|
116
|
+
cocktail: session.cocktail,
|
|
117
|
+
ownerId: session.ownerId || null,
|
|
118
|
+
// Which CLI to resume with if this session has to be re-adopted after a
|
|
119
|
+
// restart: the orphan it becomes inherits it (recoverCrashedSessions).
|
|
120
|
+
// null when nobody knows, so the fallbacks get to answer.
|
|
121
|
+
agent: sessionAgent(session),
|
|
122
|
+
permissionFlags: sessionPermissionFlags(session),
|
|
123
|
+
origin: sessionOrigin(session),
|
|
124
|
+
savedAt: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
saveConfig(broadcast);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The session's own note when it carries one (every PTY session does, see
|
|
130
|
+
// createSessionFromConfig); read off the command otherwise.
|
|
131
|
+
export function sessionAgent(session) {
|
|
132
|
+
return session.agent !== undefined ? session.agent : sessionAgentFromCommand(session.command);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The permission flags the session was spawned with (see createSessionFromConfig);
|
|
136
|
+
// read off the command for a session object that does not carry them.
|
|
137
|
+
// 'board' for a session dispatched by the board, or re-adopted from one, else 'user'.
|
|
138
|
+
export function sessionOrigin(session) {
|
|
139
|
+
return session.origin === 'board' || (session.origin === undefined && session.spawnedBy === 'board') ? 'board' : 'user';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function sessionPermissionFlags(session) {
|
|
143
|
+
return Array.isArray(session.permissionFlags) ? session.permissionFlags : permissionFlagsFromCommand(session.command);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function removeActiveSession(worktreePath, broadcast) {
|
|
147
|
+
if (!worktreePath) return;
|
|
148
|
+
config.activeSessions = config.activeSessions.filter(s => s.worktreePath !== worktreePath);
|
|
149
|
+
saveConfig(broadcast);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function recoverCrashedSessions(broadcast) {
|
|
153
|
+
const crashed = config.activeSessions || [];
|
|
154
|
+
if (crashed.length === 0) return;
|
|
155
|
+
for (const s of crashed) {
|
|
156
|
+
if (!existsSync(s.worktreePath)) continue;
|
|
157
|
+
if ([...orphans.values()].some(o => o.worktreePath === s.worktreePath)) continue;
|
|
158
|
+
const orphanId = `orphan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
159
|
+
const orphan = {
|
|
160
|
+
id: orphanId,
|
|
161
|
+
name: s.name,
|
|
162
|
+
repoPath: s.repoPath,
|
|
163
|
+
repoSlug: s.repoSlug,
|
|
164
|
+
worktreePath: s.worktreePath,
|
|
165
|
+
branchName: s.branchName,
|
|
166
|
+
color: s.color,
|
|
167
|
+
ownerId: s.ownerId || null,
|
|
168
|
+
// Absent in a config written before it was recorded; a value that is not
|
|
169
|
+
// one of ours (config.json is hand-editable) is dropped rather than
|
|
170
|
+
// carried forward as a note.
|
|
171
|
+
agent: isValidJobAgent(s.agent) ? s.agent : null,
|
|
172
|
+
permissionFlags: recordedPermissionFlags(s),
|
|
173
|
+
origin: s.origin === 'board' ? 'board' : 'user',
|
|
174
|
+
reason: 'server-restart',
|
|
175
|
+
createdAt: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
orphans.set(orphanId, orphan);
|
|
178
|
+
codenamePool.addUsed(s.name);
|
|
179
|
+
codenamePool.addUsed(basename(s.worktreePath));
|
|
180
|
+
console.log(`Recovered crashed session: ${s.name} in ${s.repoPath}`);
|
|
181
|
+
}
|
|
182
|
+
config.activeSessions = [];
|
|
183
|
+
syncOrphansToConfig(broadcast);
|
|
184
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Entry-point detection for server.js.
|
|
2
|
+
//
|
|
3
|
+
// Node builds import.meta.url for the main module from the entry file's
|
|
4
|
+
// realpath (unless --preserve-symlinks-main), while process.argv[1] keeps the
|
|
5
|
+
// path as the user typed it. A naive comparison therefore misses launches
|
|
6
|
+
// through symlinks/junctions (macOS /tmp -> /private/tmp, relocated Windows
|
|
7
|
+
// folders), and string-suffix tricks miss percent-encoded characters (spaces,
|
|
8
|
+
// ~) in the install path. Comparing file URLs of the realpathed argv covers
|
|
9
|
+
// all of these; pathToFileURL applies the exact encoding Node uses for
|
|
10
|
+
// import.meta.url.
|
|
11
|
+
import { realpathSync, statSync, readFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { pathToFileURL } from 'url';
|
|
14
|
+
|
|
15
|
+
export function isDirectRun(importMetaUrl, argv1) {
|
|
16
|
+
if (!argv1) return false;
|
|
17
|
+
// Raw comparison first: covers --preserve-symlinks-main, where the loader
|
|
18
|
+
// keeps the typed path instead of realpathing it.
|
|
19
|
+
if (importMetaUrl === pathToFileURL(argv1).href) return true;
|
|
20
|
+
try {
|
|
21
|
+
let entryPath = realpathSync(argv1);
|
|
22
|
+
if (statSync(entryPath).isDirectory()) {
|
|
23
|
+
// `node .` — the loader resolved the entry via package.json "main".
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(entryPath, 'package.json'), 'utf8'));
|
|
25
|
+
entryPath = realpathSync(join(entryPath, pkg.main || 'index.js'));
|
|
26
|
+
}
|
|
27
|
+
return importMetaUrl === pathToFileURL(entryPath).href;
|
|
28
|
+
} catch {
|
|
29
|
+
// argv1 unresolvable (deleted between launch and eval, unreadable
|
|
30
|
+
// package.json) — the raw comparison above was the last word.
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|