@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c83d468784cc9
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 +28 -28
- package/dist/acp.js +90 -19
- package/dist/afk.js +17 -0
- package/dist/agents.js +87 -44
- package/dist/api.js +5 -5
- package/dist/authority-main.js +3 -3
- package/dist/bridge.js +5 -5
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +45 -40
- package/dist/{stream.js → connection.js} +34 -52
- package/dist/conversations.js +498 -178
- package/dist/copies.js +315 -162
- package/dist/end.js +14 -4
- package/dist/events.js +66 -8
- package/dist/files.js +139 -37
- package/dist/git.js +47 -13
- package/dist/home.js +0 -7
- package/dist/house-cli.js +95 -87
- package/dist/house-main.js +3 -3
- package/dist/instructions.js +29 -9
- package/dist/kit-cli.js +4 -4
- package/dist/kit-main.js +3 -3
- package/dist/login.js +4 -4
- package/dist/logout.js +1 -1
- package/dist/mcp.js +6 -2
- package/dist/parts.js +10 -0
- package/dist/placement.js +21 -20
- package/dist/relay.js +21 -0
- package/dist/resident.js +51 -38
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +1 -1
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +42 -97
- package/dist/transcription.js +83 -43
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -127
- package/dist/wire.js +37 -3
- package/package.json +20 -21
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/how-we-work.md +0 -33
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -97
- package/skills/ask-matt/agents/openai.yaml +0 -5
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -114
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -138
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -74
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -7
- package/skills/grill-me/agents/openai.yaml +0 -5
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -7
- package/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -16
- package/skills/handoff/agents/openai.yaml +0 -5
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -17
- package/skills/implement/agents/openai.yaml +0 -5
- package/skills/implement-spec/LICENSE +0 -21
- package/skills/implement-spec/SKILL.md +0 -40
- package/skills/implement-spec/agents/openai.yaml +0 -5
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -71
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/skills/pr/CREDITS.md +0 -3
- package/skills/pr/LICENSE +0 -21
- package/skills/pr/SKILL.md +0 -170
- package/skills/pr/agents/openai.yaml +0 -3
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -26
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -12
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/retro/LICENSE +0 -21
- package/skills/retro/SKILL.md +0 -44
- package/skills/retro/agents/openai.yaml +0 -5
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -38
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -54
- package/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -75
- package/skills/to-spec/agents/openai.yaml +0 -5
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -105
- package/skills/to-tickets/agents/openai.yaml +0 -5
- package/skills/triage/AGENT-BRIEF.md +0 -207
- package/skills/triage/LICENSE +0 -21
- package/skills/triage/OUT-OF-SCOPE.md +0 -105
- package/skills/triage/SKILL.md +0 -112
- package/skills/triage/agents/openai.yaml +0 -5
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -7
- package/skills/wait-what/agents/openai.yaml +0 -5
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -128
- package/skills/wayfinder/agents/openai.yaml +0 -5
- package/skills/wizard/LICENSE +0 -21
- package/skills/wizard/SKILL.md +0 -44
- package/skills/wizard/agents/openai.yaml +0 -3
- package/skills/wizard/template.sh +0 -223
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -81
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/resident.js
CHANGED
|
@@ -8,16 +8,19 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { Agents } from './agents.js';
|
|
9
9
|
import { house } from './api.js';
|
|
10
10
|
import { received } from './bridge.js';
|
|
11
|
-
import {
|
|
11
|
+
import { CONVERSATION_METHODS, Conversations } from './conversations.js';
|
|
12
12
|
import { LocalCopies } from './copies.js';
|
|
13
|
-
import { restart } from './end.js';
|
|
14
|
-
import { agentBase, enrolled,
|
|
13
|
+
import { restart, unknownMethod } from './end.js';
|
|
14
|
+
import { agentBase, enrolled, kitHome, ownerLock, ownerSocket, readEnrolment } from './home.js';
|
|
15
|
+
import { Calls } from './calls.js';
|
|
15
16
|
import { killDescendants } from './scope.js';
|
|
17
|
+
import { removeInputs, Scripts } from './scripts.js';
|
|
16
18
|
import { quoted } from './shell.js';
|
|
17
19
|
import { SignIns } from './sign-in.js';
|
|
18
20
|
import { placeSkillSet } from './skills.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
+
import { holdConnection } from './connection.js';
|
|
22
|
+
import { KitUpdate } from './update.js';
|
|
23
|
+
import { takeUnservedRelease } from './wire.js';
|
|
21
24
|
import { codexLauncher } from './work.js';
|
|
22
25
|
function report(error) {
|
|
23
26
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -46,7 +49,7 @@ async function serveOwner(copies) {
|
|
|
46
49
|
if (!claimed(lock)) {
|
|
47
50
|
lock.close();
|
|
48
51
|
const running = await runningResident();
|
|
49
|
-
throw new Error(`${running === null ? 'another Kit' : `Kit ${running}`} already runs this Environment from Kit home ${kitHome()}; stop it first to run another here`);
|
|
52
|
+
throw new Error(`${running === null ? 'another Kit process' : `Kit process ${running}`} already runs this Environment from Kit home ${kitHome()}; stop it first to run another Kit process here`);
|
|
50
53
|
}
|
|
51
54
|
const server = createServer(async (request, response) => {
|
|
52
55
|
try {
|
|
@@ -108,14 +111,13 @@ function connected() {
|
|
|
108
111
|
}
|
|
109
112
|
async function started() {
|
|
110
113
|
await connected();
|
|
111
|
-
await forgetRestart();
|
|
112
114
|
watch(kitHome(), () => {
|
|
113
115
|
if (enrolled())
|
|
114
116
|
return;
|
|
115
117
|
killDescendants();
|
|
116
118
|
process.exit(1);
|
|
117
119
|
});
|
|
118
|
-
await placeSkillSet();
|
|
120
|
+
await placeSkillSet((await readEnrolment()).house);
|
|
119
121
|
await mkdir(agentBase(), { recursive: true });
|
|
120
122
|
await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
|
|
121
123
|
await installShim(codexLauncher(), 'codex-main');
|
|
@@ -125,71 +127,82 @@ export async function resident() {
|
|
|
125
127
|
const copies = new LocalCopies(house);
|
|
126
128
|
await copies.load();
|
|
127
129
|
await serveOwner(copies);
|
|
130
|
+
await copies.swept();
|
|
128
131
|
await started().catch((error) => {
|
|
129
132
|
report(error);
|
|
130
133
|
process.exit(1);
|
|
131
134
|
});
|
|
132
135
|
process.env.HOUSE_KIT_RESIDENT = String(process.pid);
|
|
133
136
|
const agents = new Agents(house, () => changed());
|
|
134
|
-
let
|
|
137
|
+
let connection = null;
|
|
135
138
|
let reported = false;
|
|
136
|
-
const idle = () =>
|
|
139
|
+
const idle = () => calls.idle() && conversations.idle() && agents.idle();
|
|
137
140
|
const changed = () => {
|
|
138
141
|
if (!idle()) {
|
|
139
142
|
reported = false;
|
|
140
143
|
}
|
|
141
|
-
else if (
|
|
142
|
-
restart(
|
|
144
|
+
else if (update.placedRelease() !== null && conversations.quiet()) {
|
|
145
|
+
restart(`Kit ${update.placedRelease()} is placed; its supervisor starts it`);
|
|
143
146
|
}
|
|
144
147
|
else if (!reported) {
|
|
145
148
|
reported = true;
|
|
146
|
-
logged(house.deliver('/kit/idle', {}, idle));
|
|
149
|
+
logged(house.deliver('/kit/kits/report-idle', {}, idle));
|
|
147
150
|
}
|
|
148
151
|
};
|
|
152
|
+
const calls = new Calls(house, changed);
|
|
149
153
|
const conversations = new Conversations({
|
|
150
154
|
house,
|
|
151
155
|
agents,
|
|
152
156
|
copies,
|
|
153
|
-
|
|
157
|
+
calls,
|
|
158
|
+
send: (oneWayCall) => connection?.send(oneWayCall) ?? false,
|
|
154
159
|
changed,
|
|
155
160
|
});
|
|
156
|
-
const signIns = new SignIns(house, agents,
|
|
157
|
-
const
|
|
161
|
+
const signIns = new SignIns(house, agents, calls);
|
|
162
|
+
const update = new KitUpdate(calls);
|
|
163
|
+
if (process.env.HOUSE_KIT_SANDBOX !== '1')
|
|
164
|
+
takeUnservedRelease((release) => logged(update.takeRefused(release)));
|
|
165
|
+
removeInputs();
|
|
166
|
+
const scripts = new Scripts(calls, (oneWayCall) => connection?.send(oneWayCall) ?? false);
|
|
158
167
|
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
159
168
|
process.once(signal, () => {
|
|
160
169
|
killDescendants();
|
|
170
|
+
removeInputs();
|
|
161
171
|
process.kill(process.pid, signal);
|
|
162
172
|
});
|
|
163
173
|
}
|
|
164
|
-
await house.deliver('/kit/
|
|
165
|
-
await updates.resumed().catch(report);
|
|
174
|
+
await house.deliver('/kit/kits/report-restart', {}).catch(report);
|
|
166
175
|
logged(agents.refresh());
|
|
167
|
-
|
|
176
|
+
agents.keepCurrent();
|
|
177
|
+
connection = holdConnection({
|
|
168
178
|
opened: () => {
|
|
169
179
|
conversations.opened();
|
|
170
180
|
changed();
|
|
171
181
|
logged(copies.connected());
|
|
172
182
|
},
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
else if (CONVERSATION_INPUTS.has(arrived.kind))
|
|
187
|
-
conversations.input(arrived);
|
|
188
|
-
else {
|
|
189
|
-
const refused = { refused: `This Kit does not know ${arrived.kind}; update the Kit` };
|
|
190
|
-
logged(house.deliver(`/kit/inputs/${arrived.input_id}/ack`, refused).finally(changed));
|
|
191
|
-
}
|
|
183
|
+
closed: () => scripts.disconnected(),
|
|
184
|
+
call: (call) => {
|
|
185
|
+
if (call.kind === 'sign_in')
|
|
186
|
+
signIns.start(call);
|
|
187
|
+
else if (call.kind === 'update')
|
|
188
|
+
update.take(call);
|
|
189
|
+
else if (call.kind === 'run_wizard' || call.kind === 'run_function')
|
|
190
|
+
scripts.start(call);
|
|
191
|
+
else if (CONVERSATION_METHODS.has(call.kind))
|
|
192
|
+
conversations.call(call);
|
|
193
|
+
else {
|
|
194
|
+
const refused = { refused: unknownMethod(call.kind) };
|
|
195
|
+
void calls.carry(call.input_id, async () => ({ result: refused }));
|
|
192
196
|
}
|
|
193
197
|
},
|
|
198
|
+
oneWayCall: (oneWayCall) => {
|
|
199
|
+
if (oneWayCall.type === 'watching')
|
|
200
|
+
conversations.watching(oneWayCall.watching === true);
|
|
201
|
+
if (oneWayCall.type === 'authority_changed')
|
|
202
|
+
logged(copies.changed(oneWayCall.rooms));
|
|
203
|
+
if (oneWayCall.type === 'work_available' && oneWayCall.subject === 'agents')
|
|
204
|
+
logged(agents.refresh());
|
|
205
|
+
scripts.notified(oneWayCall);
|
|
206
|
+
},
|
|
194
207
|
});
|
|
195
208
|
}
|
package/dist/scope.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
const LISTING_BYTES = 1 << 28;
|
|
5
5
|
const CGROUPS = '/sys/fs/cgroup';
|
|
6
6
|
const GONE = new Set(['ENOENT', 'ESRCH']);
|
|
7
|
+
const EXITED = new Set(['Z', 'X']);
|
|
7
8
|
function failure(error) {
|
|
8
9
|
return error.code ?? '';
|
|
9
10
|
}
|
|
@@ -24,6 +25,8 @@ function read(pid) {
|
|
|
24
25
|
try {
|
|
25
26
|
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
26
27
|
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
28
|
+
if (EXITED.has(fields[0]))
|
|
29
|
+
return null;
|
|
27
30
|
const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
|
|
28
31
|
return { parent: Number(fields[1]), session: fields[3], command };
|
|
29
32
|
}
|
|
@@ -119,15 +122,23 @@ function members(group) {
|
|
|
119
122
|
}
|
|
120
123
|
return pids;
|
|
121
124
|
}
|
|
125
|
+
function besideCli(cli) {
|
|
126
|
+
try {
|
|
127
|
+
return `${realpathSync(cli)}-`;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
122
133
|
export class Scope {
|
|
123
134
|
marker;
|
|
124
|
-
|
|
135
|
+
helpers;
|
|
125
136
|
group;
|
|
126
137
|
session = null;
|
|
127
138
|
recorded = null;
|
|
128
139
|
constructor(marker, name, cli) {
|
|
129
140
|
this.marker = marker;
|
|
130
|
-
this.
|
|
141
|
+
this.helpers = besideCli(cli);
|
|
131
142
|
const own = ownGroup();
|
|
132
143
|
this.group = own === null ? null : join(own, name);
|
|
133
144
|
try {
|
|
@@ -165,7 +176,7 @@ export class Scope {
|
|
|
165
176
|
return true;
|
|
166
177
|
if (this.recorded === null)
|
|
167
178
|
return found.size > 0;
|
|
168
|
-
return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(
|
|
179
|
+
return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || (this.helpers !== null && command.startsWith(this.helpers))));
|
|
169
180
|
}
|
|
170
181
|
empty() {
|
|
171
182
|
return this.found()?.size === 0;
|
package/dist/scripts.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { spawn as spawnPiped } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { once } from 'node:events';
|
|
4
|
+
import { rmSync } from 'node:fs';
|
|
5
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { constants } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { loginPath, onLoginPath } from './clis.js';
|
|
9
|
+
import { kitHome } from './home.js';
|
|
10
|
+
import { childEnvironment } from './relay.js';
|
|
11
|
+
import { killTree } from './scope.js';
|
|
12
|
+
const TERMINAL = 'xterm-256color';
|
|
13
|
+
const FIRST_SIZE = { cols: 80, rows: 24 };
|
|
14
|
+
const SIGNALLED = 128;
|
|
15
|
+
const NEWLINE = 0x0a;
|
|
16
|
+
const NO_BASH = "bash is not on the login shell's PATH";
|
|
17
|
+
const LOST = { refused: 'the Kit lost its connection to House' };
|
|
18
|
+
function causeOf(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
function failed(plan, cause) {
|
|
22
|
+
process.stderr.write(`kit: ${plan.named} could not start: ${cause}\n`);
|
|
23
|
+
return { refused: `the script could not start: ${cause}` };
|
|
24
|
+
}
|
|
25
|
+
function planned(call) {
|
|
26
|
+
if (call.kind === 'run_wizard')
|
|
27
|
+
return { named: 'a Web Wizard', script: call.script, terminal: 'page', function: null };
|
|
28
|
+
return {
|
|
29
|
+
named: 'an Environment function',
|
|
30
|
+
script: call.script,
|
|
31
|
+
terminal: call.terminal ? 'at once' : null,
|
|
32
|
+
function: { input: call.input, timeCap: call.time_cap, outputCap: call.output_cap },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function inputs() {
|
|
36
|
+
return join(kitHome(), 'inputs');
|
|
37
|
+
}
|
|
38
|
+
async function inputFile(value) {
|
|
39
|
+
await mkdir(inputs(), { recursive: true, mode: 0o700 });
|
|
40
|
+
const path = join(inputs(), `${randomUUID()}.json`);
|
|
41
|
+
await writeFile(path, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
export function removeInputs() {
|
|
45
|
+
rmSync(inputs(), { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
function struck(line, typed) {
|
|
48
|
+
return typed.some((typedLine) => line.includes(typedLine));
|
|
49
|
+
}
|
|
50
|
+
class KeptOutput {
|
|
51
|
+
cap;
|
|
52
|
+
typedText = '';
|
|
53
|
+
text = '';
|
|
54
|
+
bytes = 0;
|
|
55
|
+
line = '';
|
|
56
|
+
lineBytes = 0;
|
|
57
|
+
dropping = false;
|
|
58
|
+
cut = false;
|
|
59
|
+
constructor(cap) {
|
|
60
|
+
this.cap = cap;
|
|
61
|
+
}
|
|
62
|
+
typed(data) {
|
|
63
|
+
this.typedText += data;
|
|
64
|
+
}
|
|
65
|
+
add(data) {
|
|
66
|
+
const pieces = data.split('\n');
|
|
67
|
+
const rest = pieces.pop();
|
|
68
|
+
for (const piece of pieces)
|
|
69
|
+
this.ended(piece);
|
|
70
|
+
this.extend(rest);
|
|
71
|
+
}
|
|
72
|
+
kept() {
|
|
73
|
+
const typed = this.typedLines(true);
|
|
74
|
+
this.text = [...this.text.split(/(?<=\n)/), this.line].filter((line) => line !== '' && !struck(line, typed)).join('');
|
|
75
|
+
this.bytes = Buffer.byteLength(this.text);
|
|
76
|
+
this.trim();
|
|
77
|
+
return { output: this.text, cut: this.cut };
|
|
78
|
+
}
|
|
79
|
+
typedLines(all) {
|
|
80
|
+
const lines = this.typedText.split(/\r\n|\r|\n/);
|
|
81
|
+
if (!all)
|
|
82
|
+
lines.pop();
|
|
83
|
+
return lines.filter((line) => line !== '');
|
|
84
|
+
}
|
|
85
|
+
ended(piece) {
|
|
86
|
+
const line = `${this.line}${piece}\n`;
|
|
87
|
+
const dropped = this.dropping;
|
|
88
|
+
this.line = '';
|
|
89
|
+
this.lineBytes = 0;
|
|
90
|
+
this.dropping = false;
|
|
91
|
+
if (dropped || struck(line, this.typedLines(false)))
|
|
92
|
+
return;
|
|
93
|
+
this.text += line;
|
|
94
|
+
this.bytes += Buffer.byteLength(line);
|
|
95
|
+
if (this.bytes > 2 * this.cap)
|
|
96
|
+
this.trim();
|
|
97
|
+
}
|
|
98
|
+
extend(piece) {
|
|
99
|
+
if (this.dropping)
|
|
100
|
+
return;
|
|
101
|
+
this.line += piece;
|
|
102
|
+
this.lineBytes += Buffer.byteLength(piece);
|
|
103
|
+
if (this.lineBytes <= this.cap)
|
|
104
|
+
return;
|
|
105
|
+
this.text = '';
|
|
106
|
+
this.bytes = 0;
|
|
107
|
+
this.line = '';
|
|
108
|
+
this.lineBytes = 0;
|
|
109
|
+
this.dropping = true;
|
|
110
|
+
this.cut = true;
|
|
111
|
+
}
|
|
112
|
+
trim() {
|
|
113
|
+
if (this.bytes <= this.cap)
|
|
114
|
+
return;
|
|
115
|
+
const bytes = Buffer.from(this.text);
|
|
116
|
+
const start = bytes.indexOf(NEWLINE, bytes.length - this.cap - 1) + 1;
|
|
117
|
+
this.text = bytes.subarray(start).toString('utf8');
|
|
118
|
+
this.bytes = bytes.length - start;
|
|
119
|
+
this.cut = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export class Scripts {
|
|
123
|
+
calls;
|
|
124
|
+
send;
|
|
125
|
+
runs = new Map();
|
|
126
|
+
constructor(calls, send) {
|
|
127
|
+
this.calls = calls;
|
|
128
|
+
this.send = send;
|
|
129
|
+
}
|
|
130
|
+
start(call) {
|
|
131
|
+
void this.calls.carry(call.input_id, () => this.run(call.input_id, planned(call)));
|
|
132
|
+
}
|
|
133
|
+
notified(oneWayCall) {
|
|
134
|
+
const run = this.runs.get(String(oneWayCall.wizard));
|
|
135
|
+
if (run === undefined)
|
|
136
|
+
return;
|
|
137
|
+
if (oneWayCall.type === 'wizard_input')
|
|
138
|
+
this.typed(run, String(oneWayCall.data));
|
|
139
|
+
if (oneWayCall.type === 'wizard_size')
|
|
140
|
+
this.resized(run, { cols: Number(oneWayCall.cols), rows: Number(oneWayCall.rows) });
|
|
141
|
+
if (oneWayCall.type === 'wizard_end' && run.plan.terminal === 'page')
|
|
142
|
+
this.end(run, {});
|
|
143
|
+
}
|
|
144
|
+
disconnected() {
|
|
145
|
+
for (const run of this.runs.values())
|
|
146
|
+
this.end(run, LOST);
|
|
147
|
+
}
|
|
148
|
+
async run(id, plan) {
|
|
149
|
+
const size = plan.terminal === 'page' ? null : FIRST_SIZE;
|
|
150
|
+
const output = plan.function === null ? null : new KeptOutput(plan.function.outputCap);
|
|
151
|
+
const run = { plan, input: null, output, process: null, pipes: [], terminal: null, size, typed: [], ended: null, sized: Promise.withResolvers() };
|
|
152
|
+
if (size !== null)
|
|
153
|
+
run.sized.resolve();
|
|
154
|
+
this.runs.set(id, run);
|
|
155
|
+
try {
|
|
156
|
+
return { result: await this.ran(id, run), release: plan.function !== null };
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
this.runs.delete(id);
|
|
160
|
+
if (run.input !== null)
|
|
161
|
+
await rm(run.input, { force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async ran(id, run) {
|
|
165
|
+
const { plan, output } = run;
|
|
166
|
+
let exited;
|
|
167
|
+
try {
|
|
168
|
+
const { spawn } = await import('@lydell/node-pty');
|
|
169
|
+
const shell = await onLoginPath('bash');
|
|
170
|
+
if (shell === null)
|
|
171
|
+
return failed(plan, NO_BASH);
|
|
172
|
+
const path = await loginPath();
|
|
173
|
+
await run.sized.promise;
|
|
174
|
+
if (plan.function !== null)
|
|
175
|
+
run.input = await inputFile(plan.function.input);
|
|
176
|
+
if (run.ended !== null)
|
|
177
|
+
return run.ended;
|
|
178
|
+
const environment = { ...childEnvironment(), PATH: path, ...(run.input === null ? {} : { HOUSE_INPUT: run.input }) };
|
|
179
|
+
if (plan.terminal === null) {
|
|
180
|
+
({ exited } = await this.piped(plan.script, shell, environment, run));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const terminal = spawn(shell, ['-c', plan.script], { name: TERMINAL, ...run.size, cwd: process.cwd(), env: environment });
|
|
184
|
+
run.process = terminal;
|
|
185
|
+
run.terminal = terminal;
|
|
186
|
+
terminal.onData((data) => {
|
|
187
|
+
this.send({ type: 'wizard_output', wizard: id, data });
|
|
188
|
+
output?.add(data);
|
|
189
|
+
});
|
|
190
|
+
for (const data of run.typed.splice(0))
|
|
191
|
+
terminal.write(data);
|
|
192
|
+
exited = new Promise((resolve) => terminal.onExit(({ exitCode, signal }) => resolve(signal ? SIGNALLED + signal : exitCode)));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return failed(plan, causeOf(error));
|
|
197
|
+
}
|
|
198
|
+
let timedOut = false;
|
|
199
|
+
// House sends a function run's time cap in seconds, and a run still going at its cap ends with its process group and the output pipes it holds.
|
|
200
|
+
const capped = plan.function === null ? undefined : setTimeout(() => {
|
|
201
|
+
timedOut = true;
|
|
202
|
+
this.stop(run);
|
|
203
|
+
}, plan.function.timeCap * 1000);
|
|
204
|
+
const code = await exited;
|
|
205
|
+
clearTimeout(capped);
|
|
206
|
+
if (run.ended !== null)
|
|
207
|
+
return run.ended;
|
|
208
|
+
if (output === null)
|
|
209
|
+
return { exit_code: code };
|
|
210
|
+
const kept = output.kept();
|
|
211
|
+
return timedOut ? { timed_out: true, ...kept } : { exit_code: code, ...kept };
|
|
212
|
+
}
|
|
213
|
+
async piped(script, shell, environment, run) {
|
|
214
|
+
const child = spawnPiped(shell, ['-c', script], { cwd: process.cwd(), env: environment, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
215
|
+
const exited = new Promise((resolve) => child.once('close', (code, signal) => resolve(signal === null ? code : SIGNALLED + constants.signals[signal])));
|
|
216
|
+
await once(child, 'spawn');
|
|
217
|
+
run.process = { pid: child.pid };
|
|
218
|
+
run.pipes = [child.stdout, child.stderr];
|
|
219
|
+
for (const pipe of run.pipes) {
|
|
220
|
+
pipe.setEncoding('utf8');
|
|
221
|
+
pipe.on('data', (data) => run.output.add(data));
|
|
222
|
+
}
|
|
223
|
+
if (run.ended !== null)
|
|
224
|
+
this.stop(run);
|
|
225
|
+
return { exited };
|
|
226
|
+
}
|
|
227
|
+
typed(run, data) {
|
|
228
|
+
run.output?.typed(data);
|
|
229
|
+
if (run.terminal === null)
|
|
230
|
+
run.typed.push(data);
|
|
231
|
+
else
|
|
232
|
+
run.terminal.write(data);
|
|
233
|
+
}
|
|
234
|
+
resized(run, size) {
|
|
235
|
+
run.size = size;
|
|
236
|
+
run.sized.resolve();
|
|
237
|
+
try {
|
|
238
|
+
run.terminal?.resize(size.cols, size.rows);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
end(run, result) {
|
|
243
|
+
if (run.ended !== null)
|
|
244
|
+
return;
|
|
245
|
+
run.ended = result;
|
|
246
|
+
run.sized.resolve();
|
|
247
|
+
this.stop(run);
|
|
248
|
+
}
|
|
249
|
+
stop(run) {
|
|
250
|
+
if (run.process !== null)
|
|
251
|
+
killTree(run.process);
|
|
252
|
+
for (const pipe of run.pipes)
|
|
253
|
+
pipe.destroy();
|
|
254
|
+
}
|
|
255
|
+
}
|
package/dist/secret-input.js
CHANGED
|
@@ -2,7 +2,7 @@ import { finalRefusal, retryDelay } from './api.js';
|
|
|
2
2
|
export async function holdSecretInput(house, subject, steps, waiting) {
|
|
3
3
|
for (let failures = 0; waiting();) {
|
|
4
4
|
try {
|
|
5
|
-
const held = await house.post(
|
|
5
|
+
const held = await house.post('/kit/secret-inputs/await', { subject, steps });
|
|
6
6
|
if (held.outcome === 'collected')
|
|
7
7
|
return held.content;
|
|
8
8
|
if (held.release !== 'held')
|
package/dist/service.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { kitHome } from './home.js';
|
|
2
5
|
const UNIT = 'house-kit.service';
|
|
3
6
|
// launchctl and systemctl answer at once, so ten seconds only bounds a stuck service manager.
|
|
4
7
|
const MANAGER_MS = 10_000;
|
|
@@ -10,7 +13,21 @@ function start(command, args) {
|
|
|
10
13
|
if (started.status === 0)
|
|
11
14
|
return;
|
|
12
15
|
const cause = started.error?.message ?? (started.stderr.trim() || `${command} exited with ${started.status ?? 'a signal'}`);
|
|
13
|
-
throw new Error(`the
|
|
16
|
+
throw new Error(`the Kit service could not be started: ${cause}`);
|
|
17
|
+
}
|
|
18
|
+
function physical(path) {
|
|
19
|
+
return existsSync(path) ? realpathSync(path) : resolve(path);
|
|
20
|
+
}
|
|
21
|
+
function systemdHome(environment) {
|
|
22
|
+
for (const [, quoted, bare] of environment.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)) {
|
|
23
|
+
const assignment = bare ?? quoted.replace(/\\(.)/g, '$1');
|
|
24
|
+
if (assignment.startsWith('HOUSE_KIT_HOME='))
|
|
25
|
+
return assignment.slice('HOUSE_KIT_HOME='.length);
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function runsThisHome(served) {
|
|
30
|
+
return served !== undefined && physical(served) === physical(kitHome());
|
|
14
31
|
}
|
|
15
32
|
export function startService() {
|
|
16
33
|
if (process.platform === 'darwin') {
|
|
@@ -18,12 +35,17 @@ export function startService() {
|
|
|
18
35
|
const printed = manage('launchctl', ['print', service]);
|
|
19
36
|
if (printed.status !== 0 || /^\tstate = running$/m.test(printed.stdout))
|
|
20
37
|
return false;
|
|
38
|
+
if (!runsThisHome(/^\t+HOUSE_KIT_HOME => (.+)$/m.exec(printed.stdout)?.[1]))
|
|
39
|
+
return false;
|
|
21
40
|
start('launchctl', ['kickstart', service]);
|
|
22
41
|
return true;
|
|
23
42
|
}
|
|
24
43
|
for (const scope of [['--user'], []]) {
|
|
25
44
|
if (manage('systemctl', [...scope, 'is-enabled', '--quiet', UNIT]).status !== 0)
|
|
26
45
|
continue;
|
|
46
|
+
const environment = manage('systemctl', [...scope, 'show', '--property=Environment', '--value', UNIT]).stdout;
|
|
47
|
+
if (!runsThisHome(systemdHome(environment)))
|
|
48
|
+
continue;
|
|
27
49
|
if (manage('systemctl', [...scope, 'is-active', '--quiet', UNIT]).status === 0)
|
|
28
50
|
return false;
|
|
29
51
|
start('systemctl', [...scope, 'start', UNIT]);
|
package/dist/sign-in.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { CLIS
|
|
3
|
+
import { CLIS } from './clis.js';
|
|
4
|
+
import { childEnvironment } from './relay.js';
|
|
4
5
|
import { killTree } from './scope.js';
|
|
5
6
|
import { holdSecretInput } from './secret-input.js';
|
|
6
7
|
const ESCAPE = /\u001b\[[\d;]*m/g;
|
|
@@ -9,72 +10,54 @@ const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
|
|
|
9
10
|
function causeOf(error) {
|
|
10
11
|
return error instanceof Error ? error.message : String(error);
|
|
11
12
|
}
|
|
12
|
-
function logged(error) {
|
|
13
|
-
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
14
|
-
}
|
|
15
13
|
function endOf(signal) {
|
|
16
14
|
return new Promise((resolve) => signal.addEventListener('abort', () => resolve(true), { once: true }));
|
|
17
15
|
}
|
|
18
16
|
export class SignIns {
|
|
19
17
|
house;
|
|
20
18
|
agents;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
constructor(house, agents, changed) {
|
|
19
|
+
calls;
|
|
20
|
+
constructor(house, agents, calls) {
|
|
24
21
|
this.house = house;
|
|
25
22
|
this.agents = agents;
|
|
26
|
-
this.
|
|
27
|
-
}
|
|
28
|
-
idle() {
|
|
29
|
-
return this.running.size === 0;
|
|
23
|
+
this.calls = calls;
|
|
30
24
|
}
|
|
31
|
-
start(
|
|
32
|
-
|
|
33
|
-
return;
|
|
34
|
-
this.running.add(input.input_id);
|
|
35
|
-
this.changed();
|
|
36
|
-
this.signIn(input)
|
|
37
|
-
.catch(logged)
|
|
38
|
-
.finally(() => {
|
|
39
|
-
this.running.delete(input.input_id);
|
|
40
|
-
this.changed();
|
|
41
|
-
});
|
|
25
|
+
start(call) {
|
|
26
|
+
void this.calls.carry(call.input_id, () => this.signIn(call));
|
|
42
27
|
}
|
|
43
|
-
async signIn(
|
|
28
|
+
async signIn(call) {
|
|
44
29
|
const ending = new AbortController();
|
|
45
30
|
// 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(),
|
|
47
|
-
const ended = await Promise.race([this.signedIn(
|
|
31
|
+
const timer = setTimeout(() => ending.abort(), call.ends_in * 1000);
|
|
32
|
+
const ended = await Promise.race([this.signedIn(call, ending.signal), endOf(ending.signal)]);
|
|
48
33
|
clearTimeout(timer);
|
|
49
34
|
if (ended)
|
|
50
|
-
process.stderr.write(`kit: the ${
|
|
51
|
-
|
|
52
|
-
if (!ended)
|
|
53
|
-
await this.agents.reread(input.cli);
|
|
35
|
+
process.stderr.write(`kit: the ${call.cli} sign-in reached the end House set for it\n`);
|
|
36
|
+
return ended ? { result: {} } : { result: {}, returned: () => this.agents.reread(call.cli) };
|
|
54
37
|
}
|
|
55
|
-
async signedIn(
|
|
56
|
-
await this.login(
|
|
38
|
+
async signedIn(call, signal) {
|
|
39
|
+
await this.login(call, signal);
|
|
57
40
|
if (!signal.aborted)
|
|
58
|
-
await this.agents.recheck(
|
|
41
|
+
await this.agents.recheck(call.cli, signal);
|
|
59
42
|
return false;
|
|
60
43
|
}
|
|
61
|
-
async login(
|
|
44
|
+
async login(call, signal) {
|
|
62
45
|
let cli;
|
|
63
46
|
try {
|
|
64
|
-
cli = await this.agents.located(
|
|
47
|
+
cli = await this.agents.located(call.cli, signal);
|
|
65
48
|
}
|
|
66
49
|
catch (error) {
|
|
67
50
|
if (!signal.aborted)
|
|
68
|
-
process.stderr.write(`kit: the ${
|
|
51
|
+
process.stderr.write(`kit: the ${call.cli} sign-in failed: ${causeOf(error)}\n`);
|
|
69
52
|
return;
|
|
70
53
|
}
|
|
71
54
|
if (signal.aborted)
|
|
72
55
|
return;
|
|
73
|
-
const login = CLIS[
|
|
56
|
+
const login = CLIS[call.cli].login;
|
|
74
57
|
const child = spawn(cli, login.args, {
|
|
75
58
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
59
|
detached: true,
|
|
77
|
-
env:
|
|
60
|
+
env: childEnvironment(),
|
|
78
61
|
});
|
|
79
62
|
const running = () => child.exitCode === null && child.signalCode === null;
|
|
80
63
|
const stop = () => killTree(child);
|
|
@@ -102,7 +85,7 @@ export class SignIns {
|
|
|
102
85
|
return;
|
|
103
86
|
held = true;
|
|
104
87
|
const steps = [{ kind: 'visit', label: 'Open this link and sign in', url: link }, step];
|
|
105
|
-
void holdSecretInput(this.house,
|
|
88
|
+
void holdSecretInput(this.house, call.input_id, steps, running).then((content) => {
|
|
106
89
|
if (!running())
|
|
107
90
|
return;
|
|
108
91
|
if (content === null)
|
|
@@ -119,6 +102,6 @@ export class SignIns {
|
|
|
119
102
|
});
|
|
120
103
|
signal.removeEventListener('abort', stop);
|
|
121
104
|
if (failed !== null && !signal.aborted)
|
|
122
|
-
process.stderr.write(`kit: the ${
|
|
105
|
+
process.stderr.write(`kit: the ${call.cli} sign-in failed: ${failed}\n`);
|
|
123
106
|
}
|
|
124
107
|
}
|
package/dist/silence.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const SYSTEM_CLOCK = {
|
|
2
|
+
later: (delay, callback) => setTimeout(callback, delay),
|
|
3
|
+
clear: (timer) => clearTimeout(timer),
|
|
4
|
+
};
|
|
5
|
+
export class Silence {
|
|
6
|
+
timer = null;
|
|
7
|
+
interval;
|
|
8
|
+
silent;
|
|
9
|
+
clock;
|
|
10
|
+
constructor(interval, silent, clock = SYSTEM_CLOCK) {
|
|
11
|
+
this.interval = interval;
|
|
12
|
+
this.silent = silent;
|
|
13
|
+
this.clock = clock;
|
|
14
|
+
}
|
|
15
|
+
heard() {
|
|
16
|
+
this.stop();
|
|
17
|
+
this.timer = this.clock.later(this.interval, () => {
|
|
18
|
+
this.timer = null;
|
|
19
|
+
this.silent();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
stop() {
|
|
23
|
+
if (this.timer === null)
|
|
24
|
+
return;
|
|
25
|
+
this.clock.clear(this.timer);
|
|
26
|
+
this.timer = null;
|
|
27
|
+
}
|
|
28
|
+
}
|