@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c845e172df1af
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 +17 -17
- package/dist/acp.js +36 -16
- package/dist/agents.js +36 -16
- package/dist/api.js +5 -5
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +1 -1
- package/dist/calls.js +63 -0
- package/dist/clis.js +1 -1
- package/dist/{stream.js → connection.js} +32 -30
- package/dist/conversations.js +242 -116
- package/dist/copies.js +53 -51
- package/dist/end.js +6 -2
- package/dist/events.js +2 -2
- package/dist/files.js +116 -27
- package/dist/git.js +1 -1
- package/dist/house-cli.js +95 -59
- package/dist/instructions.js +2 -2
- package/dist/kit-cli.js +2 -2
- package/dist/mcp.js +4 -2
- package/dist/placement.js +11 -2
- package/dist/resident.js +32 -30
- package/dist/scope.js +3 -0
- package/dist/service.js +23 -1
- package/dist/sign-in.js +19 -37
- package/dist/skills.js +3 -3
- package/dist/translate.js +2 -8
- package/dist/update.js +42 -45
- package/dist/wire.js +19 -3
- package/package.json +3 -3
package/dist/house-cli.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { request, setGlobalProxyFromEnv } from 'node:http';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
import { downloaded, storeRefusal, uploaded } from './files.js';
|
|
6
10
|
import { HOUSE_REF } from './git.js';
|
|
7
11
|
import { ownerSocket, readOwnAgent } from './home.js';
|
|
8
|
-
import { callHouse, rpc, UNREACHABLE } from './mcp.js';
|
|
9
|
-
import { refusalOf } from './refusals.js';
|
|
12
|
+
import { callHouse, rpc, UNREACHABLE, Unsettled } from './mcp.js';
|
|
13
|
+
import { refusalOf, retryable } from './refusals.js';
|
|
10
14
|
import { relayed, selectRelay } from './relay.js';
|
|
11
|
-
import { parse
|
|
15
|
+
import { parse } from 'yaml';
|
|
12
16
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
13
17
|
const PUSH_USAGE = 'usage: house push [commit] [--owner]';
|
|
14
18
|
const PUSH = "push [commit]: sends the Local copy's committed changes to House";
|
|
@@ -51,15 +55,12 @@ const MEDIA_TYPES = new Map([
|
|
|
51
55
|
['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
52
56
|
['zip', 'application/zip'],
|
|
53
57
|
]);
|
|
54
|
-
function refused(path,
|
|
58
|
+
function refused(path, status, text, local) {
|
|
55
59
|
const refusal = refusalOf(text);
|
|
56
60
|
if (refusal !== null)
|
|
57
61
|
return refusal.text;
|
|
58
62
|
if (status === 401)
|
|
59
63
|
return REVOKED;
|
|
60
|
-
const act = method === 'GET' ? 'download' : 'upload';
|
|
61
|
-
if (URL.canParse(path))
|
|
62
|
-
return `the ${act} answered ${status}; ${act} the file again`;
|
|
63
64
|
if (status === 502 && local)
|
|
64
65
|
return text;
|
|
65
66
|
return path === '/' ? unconfirmed(status) : `${path} answered ${status}`;
|
|
@@ -67,26 +68,23 @@ function refused(path, method, status, text, local) {
|
|
|
67
68
|
function unconfirmed(status) {
|
|
68
69
|
return `House answered ${status} ${UNCONFIRMED}`;
|
|
69
70
|
}
|
|
70
|
-
function
|
|
71
|
+
function bridged(path, body, to = socketPath) {
|
|
71
72
|
return new Promise((resolve, reject) => {
|
|
72
|
-
const sent = request({ socketPath: to, path, method, headers }, (answer) => {
|
|
73
|
+
const sent = request({ socketPath: to, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
|
|
73
74
|
const chunks = [];
|
|
74
75
|
answer.on('data', (chunk) => chunks.push(chunk));
|
|
75
76
|
answer.on('end', () => {
|
|
76
|
-
const
|
|
77
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
77
78
|
if (answer.statusCode === 200)
|
|
78
|
-
resolve(
|
|
79
|
+
resolve(text);
|
|
79
80
|
else
|
|
80
|
-
reject(new Error(refused(path,
|
|
81
|
+
reject(new Error(refused(path, answer.statusCode, text, answer.headers['content-type'] === undefined)));
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
sent.on('error', (error) => reject(new Error(error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
84
|
-
sent.end(
|
|
85
|
+
sent.end(JSON.stringify(body));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
async function bridged(path, body, headers, to = socketPath) {
|
|
88
|
-
return (await exchanged(path, 'POST', body, headers, to)).toString('utf8');
|
|
89
|
-
}
|
|
90
88
|
async function direct(message) {
|
|
91
89
|
const ownAgent = await readOwnAgent();
|
|
92
90
|
if (ownAgent === null)
|
|
@@ -94,11 +92,14 @@ async function direct(message) {
|
|
|
94
92
|
selectRelay(process.env.HOUSE_KIT_RELAY || ownAgent.relay);
|
|
95
93
|
setGlobalProxyFromEnv();
|
|
96
94
|
const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
|
|
97
|
-
throw new
|
|
95
|
+
throw new Unsettled(UNREACHABLE);
|
|
98
96
|
});
|
|
99
97
|
if (answer.status === 200)
|
|
100
98
|
return answer.text;
|
|
101
|
-
|
|
99
|
+
if (answer.status === 401)
|
|
100
|
+
throw new Error(DISCONNECTED);
|
|
101
|
+
const refusal = refusalOf(answer.text)?.text ?? unconfirmed(answer.status);
|
|
102
|
+
throw retryable(answer.text) === false ? new Error(refusal) : new Unsettled(refusal);
|
|
102
103
|
}
|
|
103
104
|
async function mcp(method, params) {
|
|
104
105
|
const message = rpc(method, params);
|
|
@@ -110,31 +111,51 @@ async function mcp(method, params) {
|
|
|
110
111
|
async function tools() {
|
|
111
112
|
return (await mcp('tools/list', {})).tools;
|
|
112
113
|
}
|
|
114
|
+
function unreadable(path, error) {
|
|
115
|
+
return new Error(error.code === 'EISDIR'
|
|
116
|
+
? `${path} is a folder; upload one file at a time`
|
|
117
|
+
: `${path} is not a readable file`);
|
|
118
|
+
}
|
|
113
119
|
async function localFile(path) {
|
|
114
120
|
try {
|
|
115
121
|
return await readFile(path);
|
|
116
122
|
}
|
|
117
123
|
catch (error) {
|
|
118
|
-
throw
|
|
119
|
-
? `${path} is a folder; upload one file at a time`
|
|
120
|
-
: `${path} is not a readable file`);
|
|
124
|
+
throw unreadable(path, error);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
function opened(url, method, headers, body) {
|
|
128
|
+
return new Promise((answered, failed) => {
|
|
129
|
+
const local = socketPath !== undefined;
|
|
130
|
+
const target = local ? null : relayed(url);
|
|
131
|
+
if (!local)
|
|
132
|
+
setGlobalProxyFromEnv();
|
|
133
|
+
const sent = target === null
|
|
134
|
+
? request({ socketPath, path: url, method, headers }, answered)
|
|
135
|
+
: (target.protocol === 'https:' ? httpsRequest : request)(target, { method, headers }, answered);
|
|
136
|
+
sent.on('error', (error) => failed(new Error(!local ? UNREACHABLE : error.code === 'EPERM' || error.code === 'EACCES' ? SANDBOXED : CLOSED)));
|
|
137
|
+
if (body instanceof Readable)
|
|
138
|
+
pipeline(body, sent).catch(() => undefined);
|
|
139
|
+
else
|
|
140
|
+
sent.end(body);
|
|
129
141
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
throw new Error(refused(url, method, answer.status, received.toString('utf8'), false));
|
|
142
|
+
}
|
|
143
|
+
async function textOf(answer) {
|
|
144
|
+
return Buffer.concat(await answer.toArray()).toString('utf8');
|
|
134
145
|
}
|
|
135
146
|
async function sent(url, bytes, operation) {
|
|
136
147
|
const headers = { 'content-type': 'application/octet-stream', 'x-house-byte-operation': operation };
|
|
137
|
-
|
|
148
|
+
const answer = await opened(url, 'POST', headers, bytes);
|
|
149
|
+
const text = await textOf(answer);
|
|
150
|
+
if (answer.statusCode === 200)
|
|
151
|
+
return text;
|
|
152
|
+
throw new Error(refusalOf(text)?.text ?? storeRefusal('upload', answer.statusCode).message);
|
|
153
|
+
}
|
|
154
|
+
async function sentPart(url, file, start, end) {
|
|
155
|
+
const answer = await opened(url, 'PUT', { 'content-length': String(end - start) }, end === start ? undefined : createReadStream(file, { start, end: end - 1 }));
|
|
156
|
+
await answer.toArray();
|
|
157
|
+
if (answer.statusCode !== 200)
|
|
158
|
+
throw storeRefusal('upload', answer.statusCode);
|
|
138
159
|
}
|
|
139
160
|
async function download(args) {
|
|
140
161
|
const { flags, positionals: [argument] } = accepted(args, ['--help'], 1, DOWNLOAD_USAGE);
|
|
@@ -143,37 +164,52 @@ async function download(args) {
|
|
|
143
164
|
const { file, ...selected } = argumentsOf(argument);
|
|
144
165
|
if (typeof file !== 'string' || file === '')
|
|
145
166
|
throw new Error('download needs "file", the local file to write');
|
|
146
|
-
const inspected = parse(await called('inspect', selected));
|
|
147
|
-
if (typeof inspected?.download !== 'string')
|
|
148
|
-
throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
|
|
149
|
-
const bytes = verified(await transferred(inspected.download, 'GET'), String(inspected.sha256), String(selected.path));
|
|
150
167
|
const written = resolve(file);
|
|
151
|
-
await
|
|
152
|
-
|
|
168
|
+
await downloaded(async () => {
|
|
169
|
+
const inspected = parse(await called('inspect', selected));
|
|
170
|
+
if (typeof inspected?.download !== 'string')
|
|
171
|
+
throw new Error(`${String(selected.path)} holds no binary file; shell reads its text`);
|
|
172
|
+
return { download: inspected.download, sha256: String(inspected.sha256) };
|
|
173
|
+
}, async (url) => {
|
|
174
|
+
const answer = await opened(url, 'GET', {});
|
|
175
|
+
return { status: answer.statusCode, body: answer };
|
|
176
|
+
}, written, String(selected.path));
|
|
153
177
|
return written;
|
|
154
178
|
}
|
|
155
179
|
async function described(file) {
|
|
156
|
-
const
|
|
180
|
+
const digest = createHash('sha256');
|
|
181
|
+
let bytes = 0;
|
|
182
|
+
try {
|
|
183
|
+
for await (const chunk of createReadStream(file)) {
|
|
184
|
+
digest.update(chunk);
|
|
185
|
+
bytes += chunk.length;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw unreadable(file, error);
|
|
190
|
+
}
|
|
157
191
|
const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
|
|
158
|
-
return {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
};
|
|
192
|
+
return { bytes, sha256: digest.digest('hex'), ...(mediaType === undefined ? {} : { media_type: mediaType }) };
|
|
193
|
+
}
|
|
194
|
+
async function finished(upload, room) {
|
|
195
|
+
const text = await called('upload', { upload, room_ref: room });
|
|
196
|
+
const answer = parse(text);
|
|
197
|
+
return answer !== null && ('upload' in answer || 'checking' in answer) ? answer : { text };
|
|
166
198
|
}
|
|
167
199
|
async function upload({ file, ...destination }) {
|
|
168
200
|
if (typeof file !== 'string' || file === '')
|
|
169
201
|
throw new Error('upload needs "file", a local file');
|
|
170
|
-
const
|
|
202
|
+
const description = await described(file);
|
|
171
203
|
const answer = await called('upload', { ...destination, ...description });
|
|
172
204
|
const declared = parse(answer);
|
|
173
|
-
if (declared
|
|
205
|
+
if (declared?.upload === undefined)
|
|
174
206
|
return answer;
|
|
175
|
-
const
|
|
176
|
-
|
|
207
|
+
const accepted = await uploaded(declared.upload, {
|
|
208
|
+
bytes: description.bytes,
|
|
209
|
+
send: (url, start, end) => sentPart(url, file, start, end),
|
|
210
|
+
finish: (id) => finished(id, destination.room_ref),
|
|
211
|
+
});
|
|
212
|
+
return accepted.text;
|
|
177
213
|
}
|
|
178
214
|
async function appendRecord(args) {
|
|
179
215
|
const held = new Map();
|
|
@@ -181,8 +217,8 @@ async function appendRecord(args) {
|
|
|
181
217
|
? await Promise.all(args.attachments.map(async (attachment) => {
|
|
182
218
|
if (typeof attachment !== 'string')
|
|
183
219
|
return attachment;
|
|
184
|
-
const
|
|
185
|
-
held.set(description.sha256,
|
|
220
|
+
const description = await described(attachment);
|
|
221
|
+
held.set(description.sha256, attachment);
|
|
186
222
|
return { name: basename(attachment), ...description };
|
|
187
223
|
}))
|
|
188
224
|
: args.attachments;
|
|
@@ -193,7 +229,7 @@ async function appendRecord(args) {
|
|
|
193
229
|
if (upload === undefined)
|
|
194
230
|
return answer;
|
|
195
231
|
for (const grant of upload)
|
|
196
|
-
await sent(grant.url, held.get(grant.sha256), grant.operation);
|
|
232
|
+
await sent(grant.url, await localFile(held.get(grant.sha256)), grant.operation);
|
|
197
233
|
}
|
|
198
234
|
}
|
|
199
235
|
function unexpected(argument, usage) {
|
|
@@ -220,7 +256,7 @@ async function push(args) {
|
|
|
220
256
|
throw new Error('--owner is not for Agent conversations; run house push without it');
|
|
221
257
|
if (!owner && socketPath === undefined)
|
|
222
258
|
throw new Error('outside an Agent conversation, house push needs --owner');
|
|
223
|
-
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit },
|
|
259
|
+
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, owner ? ownerSocket() : socketPath));
|
|
224
260
|
if (pushed.refused)
|
|
225
261
|
throw new Error(pushed.text);
|
|
226
262
|
return pushed.text;
|
|
@@ -228,7 +264,7 @@ async function push(args) {
|
|
|
228
264
|
async function fetched(args) {
|
|
229
265
|
if (accepted(args, ['--help'], 0, FETCH_USAGE).flags.has('--help'))
|
|
230
266
|
return FETCH_USAGE;
|
|
231
|
-
const answer = JSON.parse(await bridged('/git/fetch', {},
|
|
267
|
+
const answer = JSON.parse(await bridged('/git/fetch', {}, socketPath ?? ownerSocket()));
|
|
232
268
|
if (answer.refused)
|
|
233
269
|
throw new Error(answer.text);
|
|
234
270
|
return answer.text;
|
package/dist/instructions.js
CHANGED
|
@@ -4,9 +4,9 @@ const SOURCE_LINE = "From House:";
|
|
|
4
4
|
const HOUSE_LINE = "Work with House through the `house` CLI: run `house --help`, and `house find_command` for House's guides and formats.";
|
|
5
5
|
const FILE_LINE = 'To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command \'{"query":"app"}\'`. Never start a server.';
|
|
6
6
|
const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
|
|
7
|
-
const
|
|
7
|
+
const REQUEST_LINE = "For another Agent's help, `send_routine_request`; its mail comes back into this conversation. If an Agent of your User has no Routine to call, write one in the Private Room and `create_routine_access` with your User as its only caller.";
|
|
8
8
|
const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
|
|
9
9
|
export async function instructions(bridge, base, copies) {
|
|
10
|
-
const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE,
|
|
10
|
+
const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, REQUEST_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
|
|
11
11
|
return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
|
|
12
12
|
}
|
package/dist/kit-cli.js
CHANGED
|
@@ -11,7 +11,7 @@ const COMMANDS = new Map([
|
|
|
11
11
|
'logout',
|
|
12
12
|
{
|
|
13
13
|
usage: 'usage: kit logout',
|
|
14
|
-
about: "disconnects this computer, stops
|
|
14
|
+
about: "disconnects this computer, stops the Kit process and ends your own agent's connection",
|
|
15
15
|
operands: 0,
|
|
16
16
|
run: logout,
|
|
17
17
|
},
|
|
@@ -20,7 +20,7 @@ const COMMANDS = new Map([
|
|
|
20
20
|
'resident',
|
|
21
21
|
{
|
|
22
22
|
usage: 'usage: kit resident',
|
|
23
|
-
about: "keeps this Environment connected;
|
|
23
|
+
about: "keeps this Environment connected; Kit's service runs it",
|
|
24
24
|
operands: 0,
|
|
25
25
|
run: resident,
|
|
26
26
|
},
|
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { KIT_RELEASE } from './clis.js';
|
|
3
3
|
import { relayed } from './relay.js';
|
|
4
4
|
const MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
5
5
|
export const UNREACHABLE = 'House is unreachable; call again in a minute';
|
|
6
|
+
export class Unsettled extends Error {
|
|
7
|
+
}
|
|
6
8
|
export function rpc(method, params) {
|
|
7
9
|
return { jsonrpc: '2.0', id: randomUUID(), method, params };
|
|
8
10
|
}
|
|
@@ -13,7 +15,7 @@ export function mcpBody(message, operation) {
|
|
|
13
15
|
...message.params,
|
|
14
16
|
_meta: {
|
|
15
17
|
'io.modelcontextprotocol/protocolVersion': MCP_PROTOCOL_VERSION,
|
|
16
|
-
'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version:
|
|
18
|
+
'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version: KIT_RELEASE },
|
|
17
19
|
'io.modelcontextprotocol/clientCapabilities': {},
|
|
18
20
|
...(operation === null ? {} : { 'agents.house/agent-operation': operation }),
|
|
19
21
|
},
|
package/dist/placement.js
CHANGED
|
@@ -18,9 +18,9 @@ export async function updatesItself() {
|
|
|
18
18
|
// sudo answers at once, so ten seconds only bounds a stuck sudo.
|
|
19
19
|
return (await run('sudo', ['-n', 'true'], 10_000)).status === 0;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
function recorded(name) {
|
|
22
22
|
try {
|
|
23
|
-
return readFileSync(join(kitHome(),
|
|
23
|
+
return readFileSync(join(kitHome(), name), 'utf8').trim();
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
|
26
26
|
if (error.code === 'ENOENT')
|
|
@@ -28,3 +28,12 @@ export function installCommand() {
|
|
|
28
28
|
throw error;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
export function installCommand() {
|
|
32
|
+
return recorded('install-command');
|
|
33
|
+
}
|
|
34
|
+
export function machine() {
|
|
35
|
+
const placed = recorded('placement');
|
|
36
|
+
if (placed === 'container' || placed === 'server' || placed === 'windows')
|
|
37
|
+
return placed;
|
|
38
|
+
return process.platform === 'darwin' ? 'macos' : 'linux';
|
|
39
|
+
}
|
package/dist/resident.js
CHANGED
|
@@ -8,16 +8,17 @@ 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
13
|
import { restart } from './end.js';
|
|
14
14
|
import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
|
|
15
|
+
import { Calls } from './calls.js';
|
|
15
16
|
import { killDescendants } from './scope.js';
|
|
16
17
|
import { quoted } from './shell.js';
|
|
17
18
|
import { SignIns } from './sign-in.js';
|
|
18
19
|
import { placeSkillSet } from './skills.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
20
|
+
import { holdConnection } from './connection.js';
|
|
21
|
+
import { KitUpdate } from './update.js';
|
|
21
22
|
import { codexLauncher } from './work.js';
|
|
22
23
|
function report(error) {
|
|
23
24
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -46,7 +47,7 @@ async function serveOwner(copies) {
|
|
|
46
47
|
if (!claimed(lock)) {
|
|
47
48
|
lock.close();
|
|
48
49
|
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`);
|
|
50
|
+
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
51
|
}
|
|
51
52
|
const server = createServer(async (request, response) => {
|
|
52
53
|
try {
|
|
@@ -131,9 +132,9 @@ export async function resident() {
|
|
|
131
132
|
});
|
|
132
133
|
process.env.HOUSE_KIT_RESIDENT = String(process.pid);
|
|
133
134
|
const agents = new Agents(house, () => changed());
|
|
134
|
-
let
|
|
135
|
+
let connection = null;
|
|
135
136
|
let reported = false;
|
|
136
|
-
const idle = () =>
|
|
137
|
+
const idle = () => calls.idle() && conversations.idle() && agents.idle();
|
|
137
138
|
const changed = () => {
|
|
138
139
|
if (!idle()) {
|
|
139
140
|
reported = false;
|
|
@@ -146,15 +147,17 @@ export async function resident() {
|
|
|
146
147
|
logged(house.deliver('/kit/idle', {}, idle));
|
|
147
148
|
}
|
|
148
149
|
};
|
|
150
|
+
const calls = new Calls(house, changed);
|
|
149
151
|
const conversations = new Conversations({
|
|
150
152
|
house,
|
|
151
153
|
agents,
|
|
152
154
|
copies,
|
|
153
|
-
|
|
155
|
+
calls,
|
|
156
|
+
send: (notification) => connection?.send(notification) ?? false,
|
|
154
157
|
changed,
|
|
155
158
|
});
|
|
156
|
-
const signIns = new SignIns(house, agents,
|
|
157
|
-
const
|
|
159
|
+
const signIns = new SignIns(house, agents, calls);
|
|
160
|
+
const update = new KitUpdate(calls);
|
|
158
161
|
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
159
162
|
process.once(signal, () => {
|
|
160
163
|
killDescendants();
|
|
@@ -162,34 +165,33 @@ export async function resident() {
|
|
|
162
165
|
});
|
|
163
166
|
}
|
|
164
167
|
await house.deliver('/kit/restarted', {}).catch(report);
|
|
165
|
-
await
|
|
168
|
+
await update.resumed().catch(report);
|
|
166
169
|
logged(agents.refresh());
|
|
167
|
-
|
|
170
|
+
connection = holdConnection({
|
|
168
171
|
opened: () => {
|
|
169
172
|
conversations.opened();
|
|
170
173
|
changed();
|
|
171
174
|
logged(copies.connected());
|
|
172
175
|
},
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
signIns.start(arrived);
|
|
184
|
-
else if (arrived.kind === 'update')
|
|
185
|
-
updates.start(arrived);
|
|
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
|
-
}
|
|
176
|
+
call: (call) => {
|
|
177
|
+
if (call.kind === 'sign_in')
|
|
178
|
+
signIns.start(call);
|
|
179
|
+
else if (call.kind === 'update')
|
|
180
|
+
update.start(call);
|
|
181
|
+
else if (CONVERSATION_METHODS.has(call.kind))
|
|
182
|
+
conversations.call(call);
|
|
183
|
+
else {
|
|
184
|
+
const refused = { refused: `This Kit does not know ${call.kind}; update the Kit` };
|
|
185
|
+
void calls.carry(call.input_id, async () => ({ result: refused }));
|
|
192
186
|
}
|
|
193
187
|
},
|
|
188
|
+
notification: (notification) => {
|
|
189
|
+
if (notification.type === 'watching')
|
|
190
|
+
conversations.watching(notification.watching === true);
|
|
191
|
+
if (notification.type === 'authority_changed')
|
|
192
|
+
logged(copies.changed(notification.rooms));
|
|
193
|
+
if (notification.type === 'work_available' && notification.subject === 'agents')
|
|
194
|
+
logged(agents.refresh());
|
|
195
|
+
},
|
|
194
196
|
});
|
|
195
197
|
}
|
package/dist/scope.js
CHANGED
|
@@ -4,6 +4,7 @@ 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
|
}
|
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
|
@@ -9,68 +9,50 @@ const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
|
|
|
9
9
|
function causeOf(error) {
|
|
10
10
|
return error instanceof Error ? error.message : String(error);
|
|
11
11
|
}
|
|
12
|
-
function logged(error) {
|
|
13
|
-
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
14
|
-
}
|
|
15
12
|
function endOf(signal) {
|
|
16
13
|
return new Promise((resolve) => signal.addEventListener('abort', () => resolve(true), { once: true }));
|
|
17
14
|
}
|
|
18
15
|
export class SignIns {
|
|
19
16
|
house;
|
|
20
17
|
agents;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
constructor(house, agents, changed) {
|
|
18
|
+
calls;
|
|
19
|
+
constructor(house, agents, calls) {
|
|
24
20
|
this.house = house;
|
|
25
21
|
this.agents = agents;
|
|
26
|
-
this.
|
|
27
|
-
}
|
|
28
|
-
idle() {
|
|
29
|
-
return this.running.size === 0;
|
|
22
|
+
this.calls = calls;
|
|
30
23
|
}
|
|
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
|
-
});
|
|
24
|
+
start(call) {
|
|
25
|
+
void this.calls.carry(call.input_id, () => this.signIn(call));
|
|
42
26
|
}
|
|
43
|
-
async signIn(
|
|
27
|
+
async signIn(call) {
|
|
44
28
|
const ending = new AbortController();
|
|
45
29
|
// 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(
|
|
30
|
+
const timer = setTimeout(() => ending.abort(), call.ends_in * 1000);
|
|
31
|
+
const ended = await Promise.race([this.signedIn(call, ending.signal), endOf(ending.signal)]);
|
|
48
32
|
clearTimeout(timer);
|
|
49
33
|
if (ended)
|
|
50
|
-
process.stderr.write(`kit: the ${
|
|
51
|
-
|
|
52
|
-
if (!ended)
|
|
53
|
-
await this.agents.reread(input.cli);
|
|
34
|
+
process.stderr.write(`kit: the ${call.cli} sign-in reached the end House set for it\n`);
|
|
35
|
+
return ended ? { result: {} } : { result: {}, returned: () => this.agents.reread(call.cli) };
|
|
54
36
|
}
|
|
55
|
-
async signedIn(
|
|
56
|
-
await this.login(
|
|
37
|
+
async signedIn(call, signal) {
|
|
38
|
+
await this.login(call, signal);
|
|
57
39
|
if (!signal.aborted)
|
|
58
|
-
await this.agents.recheck(
|
|
40
|
+
await this.agents.recheck(call.cli, signal);
|
|
59
41
|
return false;
|
|
60
42
|
}
|
|
61
|
-
async login(
|
|
43
|
+
async login(call, signal) {
|
|
62
44
|
let cli;
|
|
63
45
|
try {
|
|
64
|
-
cli = await this.agents.located(
|
|
46
|
+
cli = await this.agents.located(call.cli, signal);
|
|
65
47
|
}
|
|
66
48
|
catch (error) {
|
|
67
49
|
if (!signal.aborted)
|
|
68
|
-
process.stderr.write(`kit: the ${
|
|
50
|
+
process.stderr.write(`kit: the ${call.cli} sign-in failed: ${causeOf(error)}\n`);
|
|
69
51
|
return;
|
|
70
52
|
}
|
|
71
53
|
if (signal.aborted)
|
|
72
54
|
return;
|
|
73
|
-
const login = CLIS[
|
|
55
|
+
const login = CLIS[call.cli].login;
|
|
74
56
|
const child = spawn(cli, login.args, {
|
|
75
57
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
58
|
detached: true,
|
|
@@ -102,7 +84,7 @@ export class SignIns {
|
|
|
102
84
|
return;
|
|
103
85
|
held = true;
|
|
104
86
|
const steps = [{ kind: 'visit', label: 'Open this link and sign in', url: link }, step];
|
|
105
|
-
void holdSecretInput(this.house,
|
|
87
|
+
void holdSecretInput(this.house, call.input_id, steps, running).then((content) => {
|
|
106
88
|
if (!running())
|
|
107
89
|
return;
|
|
108
90
|
if (content === null)
|
|
@@ -119,6 +101,6 @@ export class SignIns {
|
|
|
119
101
|
});
|
|
120
102
|
signal.removeEventListener('abort', stop);
|
|
121
103
|
if (failed !== null && !signal.aborted)
|
|
122
|
-
process.stderr.write(`kit: the ${
|
|
104
|
+
process.stderr.write(`kit: the ${call.cli} sign-in failed: ${failed}\n`);
|
|
123
105
|
}
|
|
124
106
|
}
|