@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c14f8446ba284
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/README.md +37 -20
- package/dist/abort.js +9 -0
- package/dist/acp.js +18 -75
- package/dist/agents.js +246 -52
- package/dist/api.js +36 -4
- package/dist/app-git.js +64 -0
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +99 -70
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +171 -71
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +324 -199
- package/dist/copies.js +70 -68
- package/dist/end.js +8 -0
- package/dist/events.js +521 -0
- package/dist/files.js +16 -1
- package/dist/git-main.js +2 -2
- package/dist/git.js +6 -2
- package/dist/home.js +32 -3
- package/dist/house-main.js +117 -44
- package/dist/instructions.js +13 -12
- package/dist/kit-main.js +7 -1
- package/dist/login.js +39 -94
- package/dist/logout.js +21 -0
- package/dist/mcp.js +39 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +64 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +10 -0
- package/dist/resident.js +46 -21
- package/dist/scope.js +203 -0
- package/dist/secret-input.js +7 -2
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +48 -11
- package/dist/skills.js +5 -10
- package/dist/stream.js +31 -6
- package/dist/transcription.js +170 -0
- package/dist/update.js +119 -0
- package/dist/work.js +348 -0
- package/how-we-work.md +10 -12
- package/package.json +4 -7
- package/dist/blocks.js +0 -51
package/dist/resident.js
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
import { once } from 'node:events';
|
|
2
|
+
import { watch } from 'node:fs';
|
|
2
3
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { createServer } from 'node:http';
|
|
4
|
-
import { extname, join } from 'node:path';
|
|
5
|
+
import { dirname, extname, join } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { killDescendants } from './acp.js';
|
|
7
7
|
import { Agents } from './agents.js';
|
|
8
8
|
import { house } from './api.js';
|
|
9
9
|
import { received } from './bridge.js';
|
|
10
10
|
import { Conversations } from './conversations.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { LocalCopies } from './copies.js';
|
|
12
|
+
import { agentBase, enrolled, kitHome } from './home.js';
|
|
13
|
+
import { killDescendants } from './scope.js';
|
|
14
|
+
import { quoted } from './shell.js';
|
|
13
15
|
import { SignIns } from './sign-in.js';
|
|
14
16
|
import { placeSkillSet } from './skills.js';
|
|
15
17
|
import { holdStream } from './stream.js';
|
|
18
|
+
import { Updates } from './update.js';
|
|
19
|
+
import { codexLauncher } from './work.js';
|
|
16
20
|
function report(error) {
|
|
17
21
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
18
22
|
}
|
|
19
23
|
function logged(work) {
|
|
20
24
|
work.catch(report);
|
|
21
25
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const main = fileURLToPath(new URL(`./git-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
27
|
-
await mkdir(join(kitHome(), 'shim'), { recursive: true });
|
|
28
|
-
await writeFile(join(kitHome(), 'shim', 'git'), `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, {
|
|
29
|
-
mode: 0o755,
|
|
30
|
-
});
|
|
26
|
+
async function installShim(path, module) {
|
|
27
|
+
const main = fileURLToPath(new URL(`./${module}${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
28
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29
|
+
await writeFile(path, `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, { mode: 0o755 });
|
|
31
30
|
}
|
|
32
31
|
async function serveOwner(copies) {
|
|
33
32
|
const socketPath = join(kitHome(), 'owner.sock');
|
|
@@ -49,18 +48,38 @@ async function serveOwner(copies) {
|
|
|
49
48
|
server.listen(socketPath);
|
|
50
49
|
await once(server, 'listening');
|
|
51
50
|
}
|
|
51
|
+
function connected() {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const watcher = watch(kitHome(), () => {
|
|
54
|
+
if (!enrolled())
|
|
55
|
+
return;
|
|
56
|
+
watcher.close();
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
if (!enrolled()) {
|
|
60
|
+
process.stderr.write('kit: this Environment is not connected; run kit login\n');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
watcher.close();
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
52
67
|
export async function resident() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
await connected();
|
|
69
|
+
watch(kitHome(), () => {
|
|
70
|
+
if (enrolled())
|
|
71
|
+
return;
|
|
72
|
+
killDescendants();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
58
75
|
await placeSkillSet();
|
|
76
|
+
await mkdir(agentBase(), { recursive: true });
|
|
59
77
|
process.env.HOUSE_KIT_RESIDENT = String(process.pid);
|
|
60
78
|
const agents = new Agents(house);
|
|
61
|
-
const copies = new
|
|
79
|
+
const copies = new LocalCopies(house);
|
|
62
80
|
await copies.load();
|
|
63
|
-
await installShim();
|
|
81
|
+
await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
|
|
82
|
+
await installShim(codexLauncher(), 'codex-main');
|
|
64
83
|
await serveOwner(copies);
|
|
65
84
|
let stream = null;
|
|
66
85
|
let reported = false;
|
|
@@ -82,6 +101,7 @@ export async function resident() {
|
|
|
82
101
|
changed,
|
|
83
102
|
});
|
|
84
103
|
const signIns = new SignIns(house, agents, changed);
|
|
104
|
+
const updates = new Updates(house);
|
|
85
105
|
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
86
106
|
process.once(signal, () => {
|
|
87
107
|
killDescendants();
|
|
@@ -89,6 +109,7 @@ export async function resident() {
|
|
|
89
109
|
});
|
|
90
110
|
}
|
|
91
111
|
await house.deliver('/kit/restarted', {}).catch(report);
|
|
112
|
+
await updates.resumed().catch(report);
|
|
92
113
|
logged(agents.refresh());
|
|
93
114
|
stream = holdStream({
|
|
94
115
|
opened: () => {
|
|
@@ -97,9 +118,11 @@ export async function resident() {
|
|
|
97
118
|
logged(copies.select());
|
|
98
119
|
},
|
|
99
120
|
frame: (arrived) => {
|
|
121
|
+
if (arrived.type === 'watching')
|
|
122
|
+
conversations.watching(arrived.watching === true);
|
|
100
123
|
if (arrived.type === 'work_available' && arrived.subject === 'agents')
|
|
101
124
|
logged(agents.refresh());
|
|
102
|
-
if (arrived.type === 'work_available' && arrived.subject === '
|
|
125
|
+
if (arrived.type === 'work_available' && arrived.subject === 'local_copy')
|
|
103
126
|
logged(copies.select());
|
|
104
127
|
if (arrived.type === 'entries') {
|
|
105
128
|
copies.entries(arrived);
|
|
@@ -108,6 +131,8 @@ export async function resident() {
|
|
|
108
131
|
reported = false;
|
|
109
132
|
if (arrived.kind === 'sign_in')
|
|
110
133
|
signIns.start(arrived);
|
|
134
|
+
else if (arrived.kind === 'update')
|
|
135
|
+
updates.start(arrived);
|
|
111
136
|
else
|
|
112
137
|
conversations.input(arrived);
|
|
113
138
|
}
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const LISTING_BYTES = 1 << 28;
|
|
5
|
+
const CGROUPS = '/sys/fs/cgroup';
|
|
6
|
+
const GONE = new Set(['ENOENT', 'ESRCH']);
|
|
7
|
+
function failure(error) {
|
|
8
|
+
return error.code ?? '';
|
|
9
|
+
}
|
|
10
|
+
function listed(environment) {
|
|
11
|
+
const found = new Map();
|
|
12
|
+
const output = execFileSync('ps', [environment ? '-AEww' : '-Aww', '-o', 'pid=,ppid=,sess=,command='], {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: LISTING_BYTES,
|
|
15
|
+
});
|
|
16
|
+
for (const line of output.split('\n')) {
|
|
17
|
+
const fields = /^\s*(\d+)\s+(\d+)\s+(\S+)\s?(.*)$/.exec(line);
|
|
18
|
+
if (fields !== null)
|
|
19
|
+
found.set(Number(fields[1]), { parent: Number(fields[2]), session: fields[3], command: fields[4] });
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
function read(pid) {
|
|
24
|
+
try {
|
|
25
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
26
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
27
|
+
const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
|
|
28
|
+
return { parent: Number(fields[1]), session: fields[3], command };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (GONE.has(failure(error)))
|
|
32
|
+
return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function processes() {
|
|
37
|
+
if (process.platform === 'darwin')
|
|
38
|
+
return listed(false);
|
|
39
|
+
const found = new Map();
|
|
40
|
+
for (const entry of readdirSync('/proc')) {
|
|
41
|
+
const running = /^\d+$/.test(entry) ? read(entry) : null;
|
|
42
|
+
if (running !== null)
|
|
43
|
+
found.set(Number(entry), running);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function descendants(roots, table = processes()) {
|
|
48
|
+
const children = new Map();
|
|
49
|
+
for (const [pid, { parent }] of table)
|
|
50
|
+
children.set(parent, [...(children.get(parent) ?? []), pid]);
|
|
51
|
+
const tree = [];
|
|
52
|
+
const pending = roots.flatMap((root) => children.get(root) ?? []);
|
|
53
|
+
while (pending.length > 0) {
|
|
54
|
+
const pid = pending.pop();
|
|
55
|
+
tree.push(pid);
|
|
56
|
+
pending.push(...(children.get(pid) ?? []));
|
|
57
|
+
}
|
|
58
|
+
return tree;
|
|
59
|
+
}
|
|
60
|
+
function marked(marker, doubted) {
|
|
61
|
+
if (process.platform === 'darwin') {
|
|
62
|
+
return [...listed(true)].filter(([, { command }]) => ` ${command} `.includes(` ${marker} `)).map(([pid]) => pid);
|
|
63
|
+
}
|
|
64
|
+
return readdirSync('/proc')
|
|
65
|
+
.filter((entry) => /^\d+$/.test(entry) && carries(entry, marker, doubted))
|
|
66
|
+
.map(Number);
|
|
67
|
+
}
|
|
68
|
+
function carries(pid, marker, doubted) {
|
|
69
|
+
try {
|
|
70
|
+
return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').includes(marker);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (GONE.has(failure(error)))
|
|
74
|
+
return false;
|
|
75
|
+
if (failure(error) !== 'EACCES')
|
|
76
|
+
throw error;
|
|
77
|
+
return doubted && statSync(`/proc/${pid}`, { throwIfNoEntry: false })?.uid === process.getuid();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function killAll(pids) {
|
|
81
|
+
for (const pid of pids) {
|
|
82
|
+
for (const target of [-pid, pid]) {
|
|
83
|
+
try {
|
|
84
|
+
process.kill(target, 'SIGKILL');
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function killTree(child) {
|
|
91
|
+
killAll([child.pid, ...descendants([child.pid])]);
|
|
92
|
+
}
|
|
93
|
+
export function killMarked(marker) {
|
|
94
|
+
const pids = marked(marker, false);
|
|
95
|
+
killAll([...pids, ...descendants(pids)]);
|
|
96
|
+
}
|
|
97
|
+
export function killDescendants() {
|
|
98
|
+
const pids = marked(`HOUSE_KIT_RESIDENT=${process.pid}`, false);
|
|
99
|
+
killAll([...pids, ...descendants([process.pid, ...pids])]);
|
|
100
|
+
}
|
|
101
|
+
function ownGroup() {
|
|
102
|
+
if (process.platform !== 'linux')
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
const line = readFileSync('/proc/self/cgroup', 'utf8')
|
|
106
|
+
.split('\n')
|
|
107
|
+
.find((entry) => entry.startsWith('0::'));
|
|
108
|
+
return line === undefined ? null : join(CGROUPS, line.slice(3));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function members(group) {
|
|
115
|
+
const pids = readFileSync(join(group, 'cgroup.procs'), 'utf8').split('\n').filter(Boolean).map(Number);
|
|
116
|
+
for (const entry of readdirSync(group, { withFileTypes: true })) {
|
|
117
|
+
if (entry.isDirectory())
|
|
118
|
+
pids.push(...members(join(group, entry.name)));
|
|
119
|
+
}
|
|
120
|
+
return pids;
|
|
121
|
+
}
|
|
122
|
+
export class Scope {
|
|
123
|
+
marker;
|
|
124
|
+
cli;
|
|
125
|
+
group;
|
|
126
|
+
session = null;
|
|
127
|
+
recorded = null;
|
|
128
|
+
constructor(marker, name, cli) {
|
|
129
|
+
this.marker = marker;
|
|
130
|
+
this.cli = cli;
|
|
131
|
+
const own = ownGroup();
|
|
132
|
+
this.group = own === null ? null : join(own, name);
|
|
133
|
+
try {
|
|
134
|
+
if (this.group !== null)
|
|
135
|
+
mkdirSync(this.group);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
this.group = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
command(command, args) {
|
|
142
|
+
if (this.group === null)
|
|
143
|
+
return [command, args];
|
|
144
|
+
return ['/bin/sh', ['-c', 'echo $$ > "$0/cgroup.procs" 2>/dev/null; exec "$@"', this.group, command, ...args]];
|
|
145
|
+
}
|
|
146
|
+
entered(pid) {
|
|
147
|
+
this.session = process.platform === 'darwin' ? (listed(false).get(pid)?.session ?? null) : String(pid);
|
|
148
|
+
if (this.group === null)
|
|
149
|
+
return;
|
|
150
|
+
const joined = readFileSync(`/proc/${pid}/cgroup`, 'utf8').split('\n').find((entry) => entry.startsWith('0::'));
|
|
151
|
+
if (joined !== undefined && join(CGROUPS, joined.slice(3)) === this.group)
|
|
152
|
+
return;
|
|
153
|
+
this.close();
|
|
154
|
+
this.group = null;
|
|
155
|
+
}
|
|
156
|
+
record() {
|
|
157
|
+
const found = this.found();
|
|
158
|
+
if (found === null)
|
|
159
|
+
return;
|
|
160
|
+
this.recorded = new Set([...found].filter(([, { session }]) => session === this.session).map(([pid]) => pid));
|
|
161
|
+
}
|
|
162
|
+
working() {
|
|
163
|
+
const found = this.found();
|
|
164
|
+
if (found === null)
|
|
165
|
+
return true;
|
|
166
|
+
if (this.recorded === null)
|
|
167
|
+
return found.size > 0;
|
|
168
|
+
return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(`${this.cli}-`)));
|
|
169
|
+
}
|
|
170
|
+
empty() {
|
|
171
|
+
return this.found()?.size === 0;
|
|
172
|
+
}
|
|
173
|
+
kill() {
|
|
174
|
+
killMarked(this.marker);
|
|
175
|
+
if (this.group === null)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
writeFileSync(join(this.group, 'cgroup.kill'), '1');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
killAll([...(this.found()?.keys() ?? [])]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
close() {
|
|
185
|
+
if (this.group === null)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
rmdirSync(this.group);
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
found() {
|
|
193
|
+
try {
|
|
194
|
+
const table = processes();
|
|
195
|
+
const roots = this.group === null ? marked(this.marker, true) : members(this.group);
|
|
196
|
+
const pids = this.group === null ? [...roots, ...descendants(roots, table)] : roots;
|
|
197
|
+
return new Map(pids.flatMap((pid) => (table.has(pid) ? [[pid, table.get(pid)]] : [])));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/dist/secret-input.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { finalRefusal, retryDelay } from './api.js';
|
|
1
2
|
export async function holdSecretInput(house, subject, steps, waiting) {
|
|
2
|
-
|
|
3
|
+
for (let failures = 0; waiting();) {
|
|
3
4
|
try {
|
|
4
5
|
const held = await house.post(`/kit/secret-input/${subject}`, { steps });
|
|
5
6
|
if (held.outcome === 'collected')
|
|
6
7
|
return held.content;
|
|
7
8
|
if (held.release !== 'held')
|
|
8
9
|
return null;
|
|
10
|
+
failures = 0;
|
|
9
11
|
}
|
|
10
12
|
catch (error) {
|
|
13
|
+
if (finalRefusal(error))
|
|
14
|
+
return null;
|
|
11
15
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
12
|
-
|
|
16
|
+
// A wait that did not reach House is held again on the jittered curve for as long as its asker still waits.
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay(failures++)));
|
|
13
18
|
}
|
|
14
19
|
}
|
|
15
20
|
return null;
|
package/dist/shell.js
ADDED
package/dist/sign-in.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { CLIS, withoutProxy } from './clis.js';
|
|
4
|
+
import { killTree } from './scope.js';
|
|
5
5
|
import { holdSecretInput } from './secret-input.js';
|
|
6
6
|
const ESCAPE = /\u001b\[[\d;]*m/g;
|
|
7
7
|
const LINK = /https:\/\/\S+/;
|
|
8
8
|
const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
|
|
9
|
+
function causeOf(error) {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
9
12
|
function logged(error) {
|
|
10
|
-
process.stderr.write(`kit: ${
|
|
13
|
+
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
14
|
+
}
|
|
15
|
+
function endOf(signal) {
|
|
16
|
+
return new Promise((resolve) => signal.addEventListener('abort', () => resolve(true), { once: true }));
|
|
11
17
|
}
|
|
12
18
|
export class SignIns {
|
|
13
19
|
house;
|
|
@@ -27,24 +33,52 @@ export class SignIns {
|
|
|
27
33
|
return;
|
|
28
34
|
this.running.add(input.input_id);
|
|
29
35
|
this.changed();
|
|
30
|
-
this.
|
|
31
|
-
.then(() => this.agents.report())
|
|
32
|
-
.then(() => this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {}))
|
|
36
|
+
this.signIn(input)
|
|
33
37
|
.catch(logged)
|
|
34
38
|
.finally(() => {
|
|
35
39
|
this.running.delete(input.input_id);
|
|
36
40
|
this.changed();
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
|
-
async
|
|
40
|
-
|
|
43
|
+
async signIn(input) {
|
|
44
|
+
const ending = new AbortController();
|
|
45
|
+
// House stops waiting for a sign-in ten minutes after it wrote it and sends the seconds left, so its install and login end with the page.
|
|
46
|
+
const timer = setTimeout(() => ending.abort(), input.ends_in * 1000);
|
|
47
|
+
const ended = await Promise.race([this.signedIn(input, ending.signal), endOf(ending.signal)]);
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
if (ended)
|
|
50
|
+
process.stderr.write(`kit: the ${input.cli} sign-in reached the end House set for it\n`);
|
|
51
|
+
await this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {});
|
|
52
|
+
if (!ended)
|
|
53
|
+
await this.agents.reread(input.cli);
|
|
54
|
+
}
|
|
55
|
+
async signedIn(input, signal) {
|
|
56
|
+
await this.login(input, signal);
|
|
57
|
+
if (!signal.aborted)
|
|
58
|
+
await this.agents.recheck(input.cli, signal);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
async login(input, signal) {
|
|
62
|
+
let cli;
|
|
63
|
+
try {
|
|
64
|
+
cli = await this.agents.located(input.cli, signal);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!signal.aborted)
|
|
68
|
+
process.stderr.write(`kit: the ${input.cli} sign-in failed: ${causeOf(error)}\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
return;
|
|
41
73
|
const login = CLIS[input.cli].login;
|
|
42
|
-
const child = spawn(
|
|
74
|
+
const child = spawn(cli, login.args, {
|
|
43
75
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
76
|
detached: true,
|
|
45
77
|
env: withoutProxy(),
|
|
46
78
|
});
|
|
47
79
|
const running = () => child.exitCode === null && child.signalCode === null;
|
|
80
|
+
const stop = () => killTree(child);
|
|
81
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
48
82
|
let link;
|
|
49
83
|
let code;
|
|
50
84
|
let last = '';
|
|
@@ -54,6 +88,8 @@ export class SignIns {
|
|
|
54
88
|
if (plain === '')
|
|
55
89
|
return;
|
|
56
90
|
last = plain;
|
|
91
|
+
if (login.code === 'collect' && plain.includes(login.rejected))
|
|
92
|
+
killTree(child);
|
|
57
93
|
link ??= LINK.exec(plain)?.[0];
|
|
58
94
|
if (CODE.test(plain))
|
|
59
95
|
code ??= plain;
|
|
@@ -79,9 +115,10 @@ export class SignIns {
|
|
|
79
115
|
createInterface({ input: output }).on('line', read);
|
|
80
116
|
const failed = await new Promise((resolve) => {
|
|
81
117
|
child.on('error', (error) => resolve(error.message));
|
|
82
|
-
child.on('close', (status,
|
|
118
|
+
child.on('close', (status, killed) => resolve(status === 0 ? null : `exited with ${status ?? killed}${last ? `: ${last}` : ''}`));
|
|
83
119
|
});
|
|
84
|
-
|
|
120
|
+
signal.removeEventListener('abort', stop);
|
|
121
|
+
if (failed !== null && !signal.aborted)
|
|
85
122
|
process.stderr.write(`kit: the ${input.cli} sign-in failed: ${failed}\n`);
|
|
86
123
|
}
|
|
87
124
|
}
|
package/dist/skills.js
CHANGED
|
@@ -3,11 +3,10 @@ import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'nod
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { parse } from 'yaml';
|
|
7
6
|
import { KIT_VERSION } from './clis.js';
|
|
8
|
-
import { refusalCode } from './copies.js';
|
|
9
7
|
import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
|
|
10
|
-
|
|
8
|
+
import { refusalOf } from './refusals.js';
|
|
9
|
+
export const HOW_WE_WORK = '/rooms/private/how-we-work.md';
|
|
11
10
|
const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
|
|
12
11
|
const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
|
|
13
12
|
const RECORD = 'skill-set.json';
|
|
@@ -63,20 +62,16 @@ export async function placeSkillSet() {
|
|
|
63
62
|
else
|
|
64
63
|
await rm(join(kitHome(), RECORD), { force: true });
|
|
65
64
|
}
|
|
66
|
-
function documentPresent(refusal) {
|
|
67
|
-
const { conflicts } = parse(refusal.slice(refusal.indexOf('\n') + 1));
|
|
68
|
-
return conflicts.some((conflict) => conflict.code === 'path_exists');
|
|
69
|
-
}
|
|
70
65
|
export async function createHowWeWork(bridge) {
|
|
71
66
|
if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
|
|
72
67
|
return;
|
|
73
68
|
const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
|
|
74
69
|
if (created.isError === true) {
|
|
75
70
|
const text = created.content.map((content) => content.text).join('\n');
|
|
76
|
-
const
|
|
77
|
-
if (UNWRITABLE.has(code))
|
|
71
|
+
const refusal = refusalOf(text);
|
|
72
|
+
if (UNWRITABLE.has(refusal?.code ?? ''))
|
|
78
73
|
return;
|
|
79
|
-
if (code !== 'edit_conflict' || !
|
|
74
|
+
if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
|
|
80
75
|
throw new Error(text);
|
|
81
76
|
}
|
|
82
77
|
await writeFile(join(kitHome(), CREATED), '');
|
package/dist/stream.js
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
+
import { subscribe } from 'node:diagnostics_channel';
|
|
2
|
+
import { finalRefusal, HouseRefusal, retryDelay } from './api.js';
|
|
3
|
+
import { end, REPLACED, SIGN_IN_AGAIN } from './end.js';
|
|
1
4
|
import { readEnrolment } from './home.js';
|
|
5
|
+
import { relayed } from './relay.js';
|
|
6
|
+
const PATH = '/kit/stream';
|
|
2
7
|
const SUBPROTOCOL = 'house.kit.stream.1';
|
|
8
|
+
const REFUSAL_HEADER = 'x-house-refusal';
|
|
3
9
|
function streamUrl(house) {
|
|
4
|
-
const url = new URL(
|
|
10
|
+
const url = new URL(PATH, house);
|
|
5
11
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
6
12
|
return url;
|
|
7
13
|
}
|
|
14
|
+
function header(headers, name) {
|
|
15
|
+
for (let index = 0; index < headers.length; index += 2) {
|
|
16
|
+
if (headers[index].toString('latin1').toLowerCase() === name)
|
|
17
|
+
return headers[index + 1].toString('latin1');
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
let refused = null;
|
|
22
|
+
subscribe('undici:request:headers', (message) => {
|
|
23
|
+
const { request, response } = message;
|
|
24
|
+
if (!request.path.endsWith(PATH))
|
|
25
|
+
return;
|
|
26
|
+
refused = new HouseRefusal(PATH, response.statusCode, header(response.headers, REFUSAL_HEADER) || response.statusText);
|
|
27
|
+
});
|
|
8
28
|
export function holdStream(handlers) {
|
|
9
29
|
let socket = null;
|
|
10
30
|
let failures = 0;
|
|
11
31
|
const connect = async () => {
|
|
12
32
|
const { house, credential } = (await readEnrolment());
|
|
13
|
-
|
|
33
|
+
refused = null;
|
|
34
|
+
const opening = new WebSocket(relayed(streamUrl(house)), {
|
|
14
35
|
protocols: [SUBPROTOCOL],
|
|
15
36
|
headers: { authorization: `Bearer ${credential}` },
|
|
16
37
|
});
|
|
@@ -29,10 +50,14 @@ export function holdStream(handlers) {
|
|
|
29
50
|
if (socket === opening)
|
|
30
51
|
socket = null;
|
|
31
52
|
process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
if (event.reason === 'replaced')
|
|
54
|
+
end(REPLACED);
|
|
55
|
+
if (event.reason === 'credential_rejected' || refused?.status === 401)
|
|
56
|
+
end(SIGN_IN_AGAIN);
|
|
57
|
+
if (finalRefusal(refused))
|
|
58
|
+
end(refused.message);
|
|
59
|
+
// A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused as retryable or left unanswered backs off on the jittered curve.
|
|
60
|
+
setTimeout(() => void connect(), opened ? 1000 + Math.random() * 2000 : retryDelay(failures++));
|
|
36
61
|
};
|
|
37
62
|
};
|
|
38
63
|
void connect();
|