@agentshouse/kit 0.2.1-alpha.10 → 0.2.1-alpha.12
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/dist/agents.js +78 -20
- package/dist/api.js +4 -1
- package/dist/bootstrap.json +5 -5
- package/dist/bridge.js +81 -19
- package/dist/clis.js +16 -4
- package/dist/conversations.js +61 -7
- package/dist/copies.js +38 -25
- package/dist/git.js +1 -1
- package/dist/house-main.js +66 -20
- package/dist/instructions.js +12 -12
- package/dist/operation.js +7 -4
- package/dist/refusals.js +41 -0
- package/dist/resident.js +2 -0
- package/dist/skills.js +4 -9
- package/how-we-work.md +2 -4
- package/package.json +1 -1
package/dist/agents.js
CHANGED
|
@@ -1,21 +1,64 @@
|
|
|
1
1
|
import { client } from '@agentclientprotocol/sdk';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { startAdapter } from './acp.js';
|
|
3
|
+
import { killTree, startAdapter } from './acp.js';
|
|
4
4
|
import { CLIS, KIT_VERSION, cliCommand, install, run } from './clis.js';
|
|
5
5
|
import { kitHome } from './home.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const SIGNED_OUT = { signed_in: false, models: [] };
|
|
7
|
+
function select(options, category) {
|
|
8
|
+
const option = options.find((candidate) => candidate.category === category);
|
|
9
|
+
return option?.type === 'select' ? option : undefined;
|
|
10
|
+
}
|
|
11
|
+
function values(options) {
|
|
12
|
+
return options.flatMap((entry) => ('group' in entry ? entry.options : [entry])).map((entry) => entry.value);
|
|
13
|
+
}
|
|
14
|
+
function causeOf(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
async function models(kind, adapter, known) {
|
|
18
|
+
const agent = adapter.connection.agent;
|
|
19
|
+
const opened = await agent.request('session/new', { cwd: kitHome(), mcpServers: [] });
|
|
20
|
+
let options = opened.configOptions ?? [];
|
|
21
|
+
const model = select(options, 'model');
|
|
22
|
+
if (model === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
const offered = [];
|
|
25
|
+
for (const value of values(model.options)) {
|
|
26
|
+
if (select(options, 'model')?.currentValue !== value) {
|
|
27
|
+
try {
|
|
28
|
+
options = (await agent.request('session/set_config_option', { sessionId: opened.sessionId, configId: model.id, value })).configOptions;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
process.stderr.write(`kit: ${kind} refused its model ${value}: ${causeOf(error)}\n`);
|
|
32
|
+
offered.push({ model: value, efforts: known.find((entry) => entry.model === value)?.efforts ?? [] });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const effort = select(options, 'thought_level');
|
|
37
|
+
offered.push({ model: value, efforts: effort === undefined ? [] : values(effort.options) });
|
|
10
38
|
}
|
|
39
|
+
return offered;
|
|
40
|
+
}
|
|
41
|
+
async function offer(kind, known) {
|
|
42
|
+
const probe = CLIS[kind].signedIn;
|
|
43
|
+
if ('command' in probe && (await run(cliCommand(kind), probe.command, 30_000)).status !== 0)
|
|
44
|
+
return SIGNED_OUT;
|
|
45
|
+
let adapter;
|
|
11
46
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
47
|
+
adapter = await startAdapter(kind, kitHome(), client({ name: '@agentshouse/kit' }));
|
|
48
|
+
if ('initializeMeta' in probe) {
|
|
49
|
+
const method = adapter.initialized._meta?.[probe.initializeMeta];
|
|
50
|
+
if (typeof method !== 'string' || method.length === 0)
|
|
51
|
+
return SIGNED_OUT;
|
|
52
|
+
}
|
|
53
|
+
return { signed_in: true, models: await models(kind, adapter, known.models) };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
process.stderr.write(`kit: ${kind} did not offer its models: ${causeOf(error)}\n`);
|
|
57
|
+
return adapter === undefined && 'initializeMeta' in probe ? known : { signed_in: true, models: known.models };
|
|
16
58
|
}
|
|
17
|
-
|
|
18
|
-
|
|
59
|
+
finally {
|
|
60
|
+
if (adapter !== undefined)
|
|
61
|
+
killTree(adapter.child);
|
|
19
62
|
}
|
|
20
63
|
}
|
|
21
64
|
async function sshHostKey() {
|
|
@@ -31,9 +74,12 @@ export class Agents {
|
|
|
31
74
|
house;
|
|
32
75
|
reading = Promise.resolve();
|
|
33
76
|
installing = Promise.resolve();
|
|
34
|
-
|
|
77
|
+
installs = new Map();
|
|
78
|
+
offers = new Map();
|
|
35
79
|
reporting = null;
|
|
36
80
|
again = false;
|
|
81
|
+
sending = Promise.resolve();
|
|
82
|
+
sent = null;
|
|
37
83
|
constructor(house) {
|
|
38
84
|
this.house = house;
|
|
39
85
|
}
|
|
@@ -62,24 +108,36 @@ export class Agents {
|
|
|
62
108
|
this.again = true;
|
|
63
109
|
return Promise.all([installing, this.reporting]).then(() => undefined);
|
|
64
110
|
}
|
|
65
|
-
|
|
111
|
+
report() {
|
|
112
|
+
const sending = this.sending.catch(() => undefined).then(() => this.send());
|
|
113
|
+
this.sending = sending;
|
|
114
|
+
return sending;
|
|
115
|
+
}
|
|
116
|
+
async send() {
|
|
66
117
|
const agents = [];
|
|
67
|
-
for (const [kind,
|
|
68
|
-
|
|
118
|
+
for (const [kind, installed] of this.installs) {
|
|
119
|
+
const offered = installed.release === null ? SIGNED_OUT : await offer(kind, this.offers.get(kind) ?? SIGNED_OUT);
|
|
120
|
+
this.offers.set(kind, offered);
|
|
121
|
+
agents.push({ kind, ...installed, ...offered });
|
|
69
122
|
}
|
|
70
123
|
const hostKey = await sshHostKey();
|
|
71
|
-
|
|
124
|
+
const body = {
|
|
72
125
|
os: await operatingSystem(),
|
|
73
126
|
kit_version: KIT_VERSION,
|
|
74
127
|
agents,
|
|
75
128
|
...(hostKey === null ? {} : { ssh_host_key: hostKey }),
|
|
76
|
-
}
|
|
129
|
+
};
|
|
130
|
+
const state = JSON.stringify(body);
|
|
131
|
+
if (state === this.sent)
|
|
132
|
+
return;
|
|
133
|
+
await this.house.deliver('/kit/agents/report', body);
|
|
134
|
+
this.sent = state;
|
|
77
135
|
}
|
|
78
136
|
async install(kinds) {
|
|
79
|
-
const
|
|
137
|
+
const installs = new Map();
|
|
80
138
|
for (const kind of kinds)
|
|
81
|
-
|
|
82
|
-
this.
|
|
139
|
+
installs.set(kind, await install(kind));
|
|
140
|
+
this.installs = installs;
|
|
83
141
|
}
|
|
84
142
|
async reports() {
|
|
85
143
|
try {
|
package/dist/api.js
CHANGED
|
@@ -8,6 +8,9 @@ export class HouseRefusal extends Error {
|
|
|
8
8
|
this.text = text;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
export function retryDelay(attempt) {
|
|
12
|
+
return Math.min(30_000, 1000 * 2 ** attempt);
|
|
13
|
+
}
|
|
11
14
|
export async function post(house, path, body, credential, signal) {
|
|
12
15
|
const answer = await fetch(new URL(path, house), {
|
|
13
16
|
method: 'POST',
|
|
@@ -38,7 +41,7 @@ export const house = {
|
|
|
38
41
|
if (error instanceof HouseRefusal && error.status < 500)
|
|
39
42
|
throw error;
|
|
40
43
|
process.stderr.write(`kit: ${path} did not reach House: ${error.message}\n`);
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt)));
|
|
42
45
|
if (!current())
|
|
43
46
|
throw error;
|
|
44
47
|
}
|
package/dist/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.2.1-alpha.
|
|
3
|
+
"version": "0.2.1-alpha.12",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"windows": {
|
|
17
|
-
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
17
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-windows.ps1)))",
|
|
18
18
|
"asset": {
|
|
19
19
|
"name": "connect-windows.ps1",
|
|
20
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.12/connect-windows.ps1"
|
|
21
21
|
},
|
|
22
22
|
"workspace": {
|
|
23
23
|
"argument": "--workspace",
|
package/dist/bridge.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
4
|
import { mkdir } from 'node:fs/promises';
|
|
4
5
|
import { createServer, request as httpRequest } from 'node:http';
|
|
5
6
|
import { request as httpsRequest } from 'node:https';
|
|
6
7
|
import { delimiter, join } from 'node:path';
|
|
7
8
|
import { parse } from 'yaml';
|
|
9
|
+
import { HouseRefusal, retryDelay } from './api.js';
|
|
8
10
|
import { KIT_VERSION } from './clis.js';
|
|
9
|
-
import { changedPaths,
|
|
11
|
+
import { changedPaths, } from './copies.js';
|
|
10
12
|
import { kitHome, readEnrolment } from './home.js';
|
|
11
|
-
import { operationId } from './operation.js';
|
|
13
|
+
import { operationId, uuidOf } from './operation.js';
|
|
14
|
+
import { refusalOf } from './refusals.js';
|
|
12
15
|
const MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
13
16
|
const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
14
|
-
const
|
|
17
|
+
const ATTACHMENTS = '/kit/attachments/upload';
|
|
15
18
|
export async function received(request) {
|
|
16
19
|
let text = '';
|
|
17
20
|
request.setEncoding('utf8');
|
|
@@ -31,16 +34,45 @@ function relay(request, response, target, authorization) {
|
|
|
31
34
|
forwarded.on('error', () => response.destroy());
|
|
32
35
|
request.pipe(forwarded);
|
|
33
36
|
}
|
|
37
|
+
const UNREACHABLE = 'House is unreachable; call again in a minute';
|
|
38
|
+
function refusalIn(text, status) {
|
|
39
|
+
return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
|
|
40
|
+
}
|
|
34
41
|
function answered(forwarded) {
|
|
35
42
|
if (forwarded.status >= 500)
|
|
36
|
-
throw new
|
|
43
|
+
throw new HouseRefusal('/', forwarded.status, forwarded.text);
|
|
37
44
|
if (forwarded.status !== 200)
|
|
38
|
-
return { refused:
|
|
45
|
+
return { refused: refusalIn(forwarded.text, forwarded.status) };
|
|
39
46
|
const answer = JSON.parse(forwarded.text);
|
|
40
47
|
if (answer.error !== undefined)
|
|
41
|
-
return { refused:
|
|
48
|
+
return { refused: refusalIn(answer.error.message, forwarded.status) };
|
|
42
49
|
const text = answer.result.content.map((content) => content.text).join('\n');
|
|
43
|
-
return answer.result.isError === true ? { refused:
|
|
50
|
+
return answer.result.isError === true ? { refused: refusalIn(text, forwarded.status) } : { text };
|
|
51
|
+
}
|
|
52
|
+
function pendingSeconds(forwarded) {
|
|
53
|
+
if (forwarded.status !== 200)
|
|
54
|
+
return null;
|
|
55
|
+
const { result } = JSON.parse(forwarded.text);
|
|
56
|
+
if (result === undefined || result.isError === true)
|
|
57
|
+
return null;
|
|
58
|
+
const text = result.content.map((content) => content.text).join('\n');
|
|
59
|
+
if (!text.startsWith('{"pending":'))
|
|
60
|
+
return null;
|
|
61
|
+
const seconds = JSON.parse(text).pending.retry_after;
|
|
62
|
+
return typeof seconds === 'number' ? seconds : null;
|
|
63
|
+
}
|
|
64
|
+
function resendAfter(forwarded, attempt) {
|
|
65
|
+
if (forwarded === null)
|
|
66
|
+
return retryDelay(attempt);
|
|
67
|
+
if (forwarded.status >= 500)
|
|
68
|
+
return refusalOf(forwarded.text) === null ? retryDelay(attempt) : null;
|
|
69
|
+
const seconds = pendingSeconds(forwarded);
|
|
70
|
+
return seconds === null ? null : seconds * 1000;
|
|
71
|
+
}
|
|
72
|
+
function versioned(conversationId, text) {
|
|
73
|
+
const file = JSON.parse(text);
|
|
74
|
+
const identity = JSON.stringify([conversationId, file.room_ref ?? null, file.name, file.sha256]);
|
|
75
|
+
return JSON.stringify({ ...file, version: uuidOf(createHash('sha256').update(identity).digest(), 8) });
|
|
44
76
|
}
|
|
45
77
|
function editAnswer(forwarded) {
|
|
46
78
|
const answer = answered(forwarded);
|
|
@@ -50,7 +82,7 @@ function editAnswer(forwarded) {
|
|
|
50
82
|
return {
|
|
51
83
|
accepted: results.flatMap((result) => [
|
|
52
84
|
...(result.from === undefined ? [] : [{ path: result.from, revision: null, content: null }]),
|
|
53
|
-
{ path: result.path, revision: result.
|
|
85
|
+
{ path: result.path, revision: result.revision ?? null },
|
|
54
86
|
]),
|
|
55
87
|
};
|
|
56
88
|
}
|
|
@@ -70,7 +102,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
70
102
|
},
|
|
71
103
|
},
|
|
72
104
|
});
|
|
73
|
-
const
|
|
105
|
+
const sent = async (message, operation) => {
|
|
74
106
|
const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
|
|
75
107
|
const answer = await fetch(new URL('/', origin), {
|
|
76
108
|
method: 'POST',
|
|
@@ -87,7 +119,22 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
87
119
|
});
|
|
88
120
|
return { status: answer.status, text: await answer.text() };
|
|
89
121
|
};
|
|
90
|
-
const
|
|
122
|
+
const send = async (message, operation) => {
|
|
123
|
+
if (operation === null)
|
|
124
|
+
return sent(message, null);
|
|
125
|
+
for (let attempt = 0;; attempt++) {
|
|
126
|
+
const forwarded = await sent(message, operation).catch((error) => {
|
|
127
|
+
if (closed.signal.aborted)
|
|
128
|
+
throw error;
|
|
129
|
+
return null;
|
|
130
|
+
});
|
|
131
|
+
const after = resendAfter(forwarded, attempt);
|
|
132
|
+
if (after === null)
|
|
133
|
+
return forwarded;
|
|
134
|
+
await delay(after, undefined, { signal: closed.signal });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const call = (name, args) => ({ jsonrpc: '2.0', id: randomUUID(), method: 'tools/call', params: { name, arguments: args } });
|
|
91
138
|
const staged = async (operation, changes) => {
|
|
92
139
|
const bytes = Buffer.from(JSON.stringify({ changes }));
|
|
93
140
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
@@ -106,24 +153,35 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
106
153
|
});
|
|
107
154
|
const text = await posted.text();
|
|
108
155
|
if (posted.status >= 500)
|
|
109
|
-
throw new
|
|
156
|
+
throw new HouseRefusal('/', posted.status, text);
|
|
110
157
|
if (!posted.ok)
|
|
111
|
-
return { refused:
|
|
158
|
+
return { refused: refusalIn(text, posted.status) };
|
|
112
159
|
const held = JSON.parse(text).transfer;
|
|
113
160
|
return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
|
|
114
161
|
};
|
|
162
|
+
let mutations = null;
|
|
163
|
+
const writes = async (tool) => {
|
|
164
|
+
mutations ??= house
|
|
165
|
+
.post('/kit/tools/mutations', {}, closed.signal)
|
|
166
|
+
.then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
|
|
167
|
+
try {
|
|
168
|
+
return (await mutations).has(tool);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
mutations = null;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
115
175
|
const forward = async (path, text) => {
|
|
116
176
|
if (path === '/') {
|
|
117
177
|
const message = JSON.parse(text);
|
|
118
178
|
const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
|
|
119
|
-
|
|
120
|
-
(await house.post('/kit/tools/mutations', {}, closed.signal)).tools.some((listed) => listed.name === tool && listed.writes);
|
|
121
|
-
return send(message, writes ? operationId() : null);
|
|
179
|
+
return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
|
|
122
180
|
}
|
|
123
181
|
const answer = await fetch(new URL(path, origin), {
|
|
124
182
|
method: 'POST',
|
|
125
183
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
|
|
126
|
-
body: text,
|
|
184
|
+
body: versioned(conversationId, text),
|
|
127
185
|
signal: closed.signal,
|
|
128
186
|
});
|
|
129
187
|
return { status: answer.status, text: await answer.text() };
|
|
@@ -152,7 +210,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
152
210
|
await copies.observed(JSON.parse(text));
|
|
153
211
|
return { status: 200, text: '{}' };
|
|
154
212
|
}
|
|
155
|
-
return
|
|
213
|
+
return path === '/' || path === ATTACHMENTS ? forward(path, text) : { status: 404, text: '' };
|
|
156
214
|
};
|
|
157
215
|
const sockets = join(kitHome(), 'bridges');
|
|
158
216
|
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
@@ -168,7 +226,11 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
168
226
|
response.writeHead(answered.status, { 'content-type': 'application/json' }).end(answered.text);
|
|
169
227
|
}
|
|
170
228
|
catch (error) {
|
|
171
|
-
|
|
229
|
+
if (error instanceof HouseRefusal) {
|
|
230
|
+
response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
|
|
172
234
|
}
|
|
173
235
|
});
|
|
174
236
|
bridge.listen(socketPath);
|
|
@@ -195,7 +257,7 @@ export async function openBridge(house, copies, conversationId, signal) {
|
|
|
195
257
|
GIT_CONFIG_VALUE_0: app,
|
|
196
258
|
},
|
|
197
259
|
tool: async (name, args) => {
|
|
198
|
-
const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id:
|
|
260
|
+
const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id: randomUUID(), method: 'tools/call', params: { name, arguments: args } }));
|
|
199
261
|
if (forwarded.status !== 200)
|
|
200
262
|
throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
|
|
201
263
|
const answer = JSON.parse(forwarded.text);
|
package/dist/clis.js
CHANGED
|
@@ -20,6 +20,14 @@ function asyncTask({ method, params }) {
|
|
|
20
20
|
}
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
+
function airPhase({ _meta }) {
|
|
24
|
+
const phase = _meta?.jetbrains?.air?.phase;
|
|
25
|
+
if (phase === 'commentary')
|
|
26
|
+
return 'note';
|
|
27
|
+
if (phase === 'final_answer')
|
|
28
|
+
return 'answer';
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
23
31
|
export const CLIS = {
|
|
24
32
|
'codex-acp': {
|
|
25
33
|
package: '@agentclientprotocol/codex-acp',
|
|
@@ -31,6 +39,7 @@ export const CLIS = {
|
|
|
31
39
|
turnStarted: (notice) => threadStatus(notice) === 'active',
|
|
32
40
|
turnEnded: (notice) => threadStatus(notice) === 'idle',
|
|
33
41
|
job: asyncTask,
|
|
42
|
+
phase: airPhase,
|
|
34
43
|
},
|
|
35
44
|
'claude-agent-acp': {
|
|
36
45
|
package: '@agentclientprotocol/claude-agent-acp',
|
|
@@ -44,6 +53,7 @@ export const CLIS = {
|
|
|
44
53
|
params?.update?.sessionUpdate === 'usage_update' &&
|
|
45
54
|
params.update._meta?.['_claude/origin'] !== undefined,
|
|
46
55
|
job: asyncTask,
|
|
56
|
+
phase: () => null,
|
|
47
57
|
},
|
|
48
58
|
'grok-build': {
|
|
49
59
|
package: '@xai-official/grok',
|
|
@@ -61,6 +71,7 @@ export const CLIS = {
|
|
|
61
71
|
return { id: params.update.task_snapshot.task_id, running: false };
|
|
62
72
|
return null;
|
|
63
73
|
},
|
|
74
|
+
phase: () => null,
|
|
64
75
|
},
|
|
65
76
|
};
|
|
66
77
|
const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -104,11 +115,12 @@ export function run(command, args, timeoutMs) {
|
|
|
104
115
|
export async function install(kind) {
|
|
105
116
|
const release = pinned(kind);
|
|
106
117
|
if ((await installedRelease(kind)) === release)
|
|
107
|
-
return release;
|
|
118
|
+
return { release, failure: null };
|
|
108
119
|
const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', `${CLIS[kind].package}@${release}`], 15 * 60_000);
|
|
109
120
|
if (ran.status !== 0) {
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
const failure = ran.output.trim() || `npm exited with ${ran.status ?? 'a signal'}`;
|
|
122
|
+
process.stderr.write(`kit: ${kind} did not install: ${failure}\n`);
|
|
123
|
+
return { release: null, failure };
|
|
112
124
|
}
|
|
113
|
-
return installedRelease(kind);
|
|
125
|
+
return { release: await installedRelease(kind), failure: null };
|
|
114
126
|
}
|
package/dist/conversations.js
CHANGED
|
@@ -36,6 +36,9 @@ function stop(bridge) {
|
|
|
36
36
|
class Turn {
|
|
37
37
|
id = randomUUID();
|
|
38
38
|
text = '';
|
|
39
|
+
segment = null;
|
|
40
|
+
admitted = false;
|
|
41
|
+
notes = [];
|
|
39
42
|
sent = [];
|
|
40
43
|
unsentFrom = null;
|
|
41
44
|
draftSequence = 0;
|
|
@@ -272,6 +275,7 @@ export class Conversations {
|
|
|
272
275
|
sessionId: opened.sessionId,
|
|
273
276
|
killed: false,
|
|
274
277
|
queues: CLIS[route.kind].queues,
|
|
278
|
+
phase: CLIS[route.kind].phase,
|
|
275
279
|
};
|
|
276
280
|
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
277
281
|
conversation.running = running;
|
|
@@ -359,6 +363,9 @@ export class Conversations {
|
|
|
359
363
|
return turn;
|
|
360
364
|
}
|
|
361
365
|
admitted(conversation, turn) {
|
|
366
|
+
turn.admitted = true;
|
|
367
|
+
for (const note of turn.notes.splice(0))
|
|
368
|
+
this.kit.send(note);
|
|
362
369
|
if (turn.text !== '') {
|
|
363
370
|
turn.unsentFrom = 0;
|
|
364
371
|
this.draft(conversation, turn);
|
|
@@ -372,6 +379,17 @@ export class Conversations {
|
|
|
372
379
|
this.kit.changed();
|
|
373
380
|
return turn;
|
|
374
381
|
}
|
|
382
|
+
finish(conversation, turn) {
|
|
383
|
+
if (turn.ended)
|
|
384
|
+
return;
|
|
385
|
+
const segment = turn.segment;
|
|
386
|
+
turn.segment = null;
|
|
387
|
+
if (segment?.phase === 'note')
|
|
388
|
+
this.note(conversation, turn, segment.text);
|
|
389
|
+
else if (segment !== null)
|
|
390
|
+
this.answer(conversation, turn, segment.text);
|
|
391
|
+
this.end(conversation, turn, { text: turn.text });
|
|
392
|
+
}
|
|
375
393
|
end(conversation, turn, outcome) {
|
|
376
394
|
if (turn.ended)
|
|
377
395
|
return;
|
|
@@ -429,7 +447,7 @@ export class Conversations {
|
|
|
429
447
|
this.end(conversation, turn, { failed });
|
|
430
448
|
}
|
|
431
449
|
else if (turn.prompt === sent) {
|
|
432
|
-
this.
|
|
450
|
+
this.finish(conversation, turn);
|
|
433
451
|
}
|
|
434
452
|
}
|
|
435
453
|
started(conversation) {
|
|
@@ -446,7 +464,7 @@ export class Conversations {
|
|
|
446
464
|
if (turn === null)
|
|
447
465
|
return;
|
|
448
466
|
if (turn.prompt === null) {
|
|
449
|
-
this.
|
|
467
|
+
this.finish(conversation, turn);
|
|
450
468
|
}
|
|
451
469
|
else {
|
|
452
470
|
conversation.turn = null;
|
|
@@ -500,17 +518,21 @@ export class Conversations {
|
|
|
500
518
|
update(conversation, notification) {
|
|
501
519
|
const update = notification.update;
|
|
502
520
|
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
503
|
-
|
|
504
|
-
turn.text += update.content.text;
|
|
505
|
-
this.draft(conversation, turn);
|
|
521
|
+
this.chunk(conversation, this.started(conversation), update, update.content.text);
|
|
506
522
|
}
|
|
507
523
|
else if (update.sessionUpdate === 'plan') {
|
|
508
524
|
const turn = this.started(conversation);
|
|
525
|
+
this.close(conversation, turn);
|
|
509
526
|
turn.entries = update.entries;
|
|
510
527
|
this.plan(conversation, turn);
|
|
511
528
|
}
|
|
512
|
-
else if (update.sessionUpdate === '
|
|
513
|
-
this.started(conversation);
|
|
529
|
+
else if (update.sessionUpdate === 'tool_call') {
|
|
530
|
+
this.close(conversation, this.started(conversation));
|
|
531
|
+
}
|
|
532
|
+
else if (update.sessionUpdate === 'agent_thought_chunk') {
|
|
533
|
+
const turn = this.started(conversation);
|
|
534
|
+
if (turn.segment?.messageId !== (update.messageId ?? null))
|
|
535
|
+
this.close(conversation, turn);
|
|
514
536
|
}
|
|
515
537
|
else if (update.sessionUpdate === 'available_commands_update') {
|
|
516
538
|
this.commands(conversation, update.availableCommands);
|
|
@@ -526,6 +548,38 @@ export class Conversations {
|
|
|
526
548
|
conversation.jobs.delete(job.id);
|
|
527
549
|
this.kit.changed();
|
|
528
550
|
}
|
|
551
|
+
chunk(conversation, turn, chunk, text) {
|
|
552
|
+
const phase = conversation.running?.phase(chunk) ?? null;
|
|
553
|
+
const messageId = chunk.messageId ?? null;
|
|
554
|
+
if (turn.segment?.messageId !== messageId || turn.segment.phase !== phase)
|
|
555
|
+
this.close(conversation, turn);
|
|
556
|
+
if (phase === 'answer') {
|
|
557
|
+
this.answer(conversation, turn, text);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
turn.segment ??= { messageId, phase, text: '' };
|
|
561
|
+
turn.segment.text += text;
|
|
562
|
+
}
|
|
563
|
+
close(conversation, turn) {
|
|
564
|
+
const segment = turn.segment;
|
|
565
|
+
if (segment === null)
|
|
566
|
+
return;
|
|
567
|
+
turn.segment = null;
|
|
568
|
+
this.note(conversation, turn, segment.text);
|
|
569
|
+
}
|
|
570
|
+
note(conversation, turn, text) {
|
|
571
|
+
if (text.trim() === '')
|
|
572
|
+
return;
|
|
573
|
+
const note = { type: 'working-note', conversation_id: conversation.id, note_id: randomUUID(), text };
|
|
574
|
+
if (turn.admitted)
|
|
575
|
+
this.kit.send(note);
|
|
576
|
+
else
|
|
577
|
+
turn.notes.push(note);
|
|
578
|
+
}
|
|
579
|
+
answer(conversation, turn, text) {
|
|
580
|
+
turn.text += text;
|
|
581
|
+
this.draft(conversation, turn);
|
|
582
|
+
}
|
|
529
583
|
draft(conversation, turn) {
|
|
530
584
|
const blocks = blocksOf(turn.text);
|
|
531
585
|
const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
|
package/dist/copies.js
CHANGED
|
@@ -6,6 +6,7 @@ import { HouseRefusal } from './api.js';
|
|
|
6
6
|
import { blobId, blobText, git, HOUSE_REF, importCommits, revision, run, succeeded, tree, WORKING_COPIES, } from './git.js';
|
|
7
7
|
import { kitHome } from './home.js';
|
|
8
8
|
import { operationId } from './operation.js';
|
|
9
|
+
import { refusalOf } from './refusals.js';
|
|
9
10
|
import { changed } from './translate.js';
|
|
10
11
|
const BRANCH = 'main';
|
|
11
12
|
const ROOM_HOMES = ['capture', 'library', 'collaboration'];
|
|
@@ -18,13 +19,16 @@ function causeOf(error) {
|
|
|
18
19
|
function logged(error) {
|
|
19
20
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const REBASED = new Set([
|
|
23
|
+
'field_base_stale',
|
|
24
|
+
'section_base_stale',
|
|
25
|
+
'preamble_base_stale',
|
|
26
|
+
'base_revision_stale',
|
|
27
|
+
'path_exists',
|
|
28
|
+
'path_absent',
|
|
29
|
+
]);
|
|
30
|
+
function rebaseFixes(refusal) {
|
|
31
|
+
return refusal.code === 'edit_conflict' && refusal.conflicts.some((code) => REBASED.has(code));
|
|
28
32
|
}
|
|
29
33
|
function short(commit) {
|
|
30
34
|
return commit.slice(0, 12);
|
|
@@ -166,7 +170,9 @@ export class WorkingCopies {
|
|
|
166
170
|
catch (error) {
|
|
167
171
|
if (!(error instanceof HouseRefusal) || error.status >= 500)
|
|
168
172
|
throw error;
|
|
169
|
-
return {
|
|
173
|
+
return {
|
|
174
|
+
refused: refusalOf(error.text) ?? { code: '', text: `House refused it with ${error.status}`, conflicts: [] },
|
|
175
|
+
};
|
|
170
176
|
}
|
|
171
177
|
},
|
|
172
178
|
};
|
|
@@ -253,11 +259,11 @@ export class WorkingCopies {
|
|
|
253
259
|
async push(cwd, selector, caller) {
|
|
254
260
|
const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
|
|
255
261
|
if (located.status !== 0)
|
|
256
|
-
return { refused: true, text: `${cwd} is not
|
|
262
|
+
return { refused: true, text: `${cwd} is not a House working copy; run house git push inside one` };
|
|
257
263
|
const [common, worktree] = located.stdout.toString('utf8').split('\n');
|
|
258
264
|
const key = await this.keyAt(dirname(common));
|
|
259
265
|
if (key === undefined)
|
|
260
|
-
return { refused: true, text: `${worktree} is not a House working copy` };
|
|
266
|
+
return { refused: true, text: `${worktree} is not a House working copy; run house git push inside one` };
|
|
261
267
|
return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
|
|
262
268
|
}
|
|
263
269
|
async keyAt(repository) {
|
|
@@ -437,7 +443,7 @@ export class WorkingCopies {
|
|
|
437
443
|
});
|
|
438
444
|
}
|
|
439
445
|
catch (error) {
|
|
440
|
-
const code = error instanceof HouseRefusal ?
|
|
446
|
+
const code = error instanceof HouseRefusal ? (refusalOf(error.text)?.code ?? null) : null;
|
|
441
447
|
if (code === 'position_expired')
|
|
442
448
|
return this.received(copy);
|
|
443
449
|
if (code !== 'replica_behind')
|
|
@@ -512,8 +518,9 @@ export class WorkingCopies {
|
|
|
512
518
|
}
|
|
513
519
|
copy = await this.save(without(copy));
|
|
514
520
|
}
|
|
515
|
-
if (!copy.active)
|
|
516
|
-
return { refused: true, text: `${rootOf(copy)} is no longer
|
|
521
|
+
if (!copy.active) {
|
|
522
|
+
return { refused: true, text: `${rootOf(copy)} is no longer synced to this Environment, so House takes no push from it` };
|
|
523
|
+
}
|
|
517
524
|
const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
|
|
518
525
|
const base = listed.find((ancestor) => ancestor in copy.manifests);
|
|
519
526
|
if (base === undefined) {
|
|
@@ -582,9 +589,8 @@ export class WorkingCopies {
|
|
|
582
589
|
async settle(start, caller, restaged = false) {
|
|
583
590
|
let copy = start;
|
|
584
591
|
const push = copy.push;
|
|
585
|
-
if (push.caller !== caller.key)
|
|
586
|
-
return this.recovered(copy
|
|
587
|
-
}
|
|
592
|
+
if (push.caller !== caller.key)
|
|
593
|
+
return this.recovered(copy);
|
|
588
594
|
let outcome;
|
|
589
595
|
try {
|
|
590
596
|
outcome = await caller.submit(push.operation_id, push.changes, push.upload, async (upload) => {
|
|
@@ -592,9 +598,10 @@ export class WorkingCopies {
|
|
|
592
598
|
});
|
|
593
599
|
}
|
|
594
600
|
catch (error) {
|
|
601
|
+
const refusal = error instanceof HouseRefusal ? refusalOf(error.text) : null;
|
|
595
602
|
return {
|
|
596
603
|
refused: true,
|
|
597
|
-
text: `House's answer to ${short(push.commit)} did not arrive
|
|
604
|
+
text: refusal?.text ?? `House's answer to ${short(push.commit)} did not arrive; run house git push again`,
|
|
598
605
|
};
|
|
599
606
|
}
|
|
600
607
|
if ('refused' in outcome) {
|
|
@@ -603,22 +610,28 @@ export class WorkingCopies {
|
|
|
603
610
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
604
611
|
}
|
|
605
612
|
if (outcome.refused.code === 'operation_expired')
|
|
606
|
-
return this.recovered(copy
|
|
613
|
+
return this.recovered(copy);
|
|
614
|
+
const rebased = rebaseFixes(outcome.refused);
|
|
607
615
|
let settled = await this.save(without(copy));
|
|
608
616
|
try {
|
|
609
617
|
settled = await this.save(await this.caughtUp(settled));
|
|
610
618
|
}
|
|
611
|
-
catch
|
|
612
|
-
return {
|
|
619
|
+
catch {
|
|
620
|
+
return {
|
|
621
|
+
refused: true,
|
|
622
|
+
text: rebased ? `${outcome.refused.text}\nHouse's current state did not arrive; push again later` : outcome.refused.text,
|
|
623
|
+
};
|
|
613
624
|
}
|
|
614
625
|
return {
|
|
615
626
|
refused: true,
|
|
616
|
-
text:
|
|
627
|
+
text: rebased
|
|
628
|
+
? `${outcome.refused.text}\n${HOUSE_REF} holds House's current state; rebase onto it and push again`
|
|
629
|
+
: outcome.refused.text,
|
|
617
630
|
};
|
|
618
631
|
}
|
|
619
632
|
return this.finished(await this.save({ ...copy, push: { ...copy.push, state: 'accepted', written: outcome.accepted } }));
|
|
620
633
|
}
|
|
621
|
-
async recovered(copy
|
|
634
|
+
async recovered(copy) {
|
|
622
635
|
const push = copy.push;
|
|
623
636
|
const current = await this.save(await this.caughtUp(copy));
|
|
624
637
|
const house = await tree(copy.repository, current.house);
|
|
@@ -627,7 +640,7 @@ export class WorkingCopies {
|
|
|
627
640
|
await this.save(without(current));
|
|
628
641
|
return {
|
|
629
642
|
refused: true,
|
|
630
|
-
text:
|
|
643
|
+
text: `House does not hold ${short(push.commit)}; run house git push again`,
|
|
631
644
|
};
|
|
632
645
|
}
|
|
633
646
|
const written = [];
|
|
@@ -652,10 +665,10 @@ export class WorkingCopies {
|
|
|
652
665
|
await this.save(without(copy));
|
|
653
666
|
return { refused: false, text: integrated.text };
|
|
654
667
|
}
|
|
655
|
-
catch
|
|
668
|
+
catch {
|
|
656
669
|
return {
|
|
657
670
|
refused: false,
|
|
658
|
-
text: `House accepted ${short(commit)};
|
|
671
|
+
text: `House accepted ${short(commit)}; run house git push again to finish integrating it here`,
|
|
659
672
|
};
|
|
660
673
|
}
|
|
661
674
|
}
|
package/dist/git.js
CHANGED
|
@@ -50,7 +50,7 @@ export async function blobText(repository, object, path) {
|
|
|
50
50
|
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
51
51
|
}
|
|
52
52
|
catch {
|
|
53
|
-
throw new Error(`${path} is
|
|
53
|
+
throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
export function blobId(content) {
|
package/dist/house-main.js
CHANGED
|
@@ -4,10 +4,25 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { request } from 'node:http';
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { kitHome } from './home.js';
|
|
7
|
+
import { refusalOf } from './refusals.js';
|
|
7
8
|
const USAGE = "usage: house <tool> ['<arguments as JSON>']";
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const GIT_USAGE = 'usage: house git push [commit] [--owner]';
|
|
10
|
+
const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
|
|
11
|
+
const CLOSED = "this conversation's House connection is closed; nothing to do from here";
|
|
12
|
+
const REVOKED = 'this conversation no longer has House access; nothing to do from here';
|
|
10
13
|
const socketPath = process.env.HOUSE_BRIDGE;
|
|
14
|
+
function refused(path, status, text) {
|
|
15
|
+
const refusal = refusalOf(text);
|
|
16
|
+
if (refusal !== null)
|
|
17
|
+
return refusal.text;
|
|
18
|
+
if (status === 401)
|
|
19
|
+
return REVOKED;
|
|
20
|
+
if (URL.canParse(path))
|
|
21
|
+
return `the upload answered ${status}; upload the file again`;
|
|
22
|
+
if (status === 502 && /^[A-Za-z]/.test(text))
|
|
23
|
+
return text;
|
|
24
|
+
return `${path} answered ${status}`;
|
|
25
|
+
}
|
|
11
26
|
function bridged(path, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
|
|
12
27
|
return new Promise((resolve, reject) => {
|
|
13
28
|
const sent = request({ socketPath: to, path, method: 'POST', headers }, (answer) => {
|
|
@@ -20,15 +35,15 @@ function bridged(path, body, headers = { 'content-type': 'application/json' }, t
|
|
|
20
35
|
if (answer.statusCode === 200)
|
|
21
36
|
resolve(text);
|
|
22
37
|
else
|
|
23
|
-
reject(new Error(
|
|
38
|
+
reject(new Error(refused(path, answer.statusCode, text)));
|
|
24
39
|
});
|
|
25
40
|
});
|
|
26
|
-
sent.on('error', reject);
|
|
41
|
+
sent.on('error', () => reject(new Error(CLOSED)));
|
|
27
42
|
sent.end(body instanceof Buffer ? body : JSON.stringify(body));
|
|
28
43
|
});
|
|
29
44
|
}
|
|
30
45
|
async function mcp(method, params) {
|
|
31
|
-
const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id:
|
|
46
|
+
const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: randomUUID(), method, params }));
|
|
32
47
|
if (answer.error !== undefined)
|
|
33
48
|
throw new Error(answer.error.message);
|
|
34
49
|
return answer.result;
|
|
@@ -36,29 +51,45 @@ async function mcp(method, params) {
|
|
|
36
51
|
async function tools() {
|
|
37
52
|
return (await mcp('tools/list', {})).tools;
|
|
38
53
|
}
|
|
39
|
-
async function
|
|
40
|
-
|
|
54
|
+
async function localFile(path) {
|
|
55
|
+
try {
|
|
56
|
+
return await readFile(path);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(error.code === 'EISDIR'
|
|
60
|
+
? `${path} is a folder; upload one file at a time`
|
|
61
|
+
: `${path} is not a readable file`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function upload(path, roomRef, missing) {
|
|
65
|
+
if (typeof path !== 'string' || path === '')
|
|
66
|
+
throw new Error(missing);
|
|
67
|
+
const bytes = await localFile(path);
|
|
41
68
|
const declared = JSON.parse(await bridged('/kit/attachments/upload', {
|
|
42
69
|
...(roomRef === undefined ? {} : { room_ref: roomRef }),
|
|
43
|
-
version: randomUUID(),
|
|
44
70
|
name: basename(path),
|
|
45
71
|
bytes: bytes.length,
|
|
46
72
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
47
73
|
}));
|
|
48
|
-
|
|
74
|
+
if (declared.upload === undefined)
|
|
75
|
+
return declared.attachment;
|
|
76
|
+
const saved = JSON.parse(await bridged(declared.upload.url, bytes, {
|
|
49
77
|
'content-type': 'application/octet-stream',
|
|
50
78
|
'x-house-byte-operation': declared.upload.operation,
|
|
51
|
-
});
|
|
79
|
+
}));
|
|
80
|
+
if (saved.upload_failure !== undefined)
|
|
81
|
+
throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
|
|
82
|
+
return saved.attachment;
|
|
52
83
|
}
|
|
53
84
|
async function gitPush(args) {
|
|
54
85
|
if (args.includes('--help'))
|
|
55
86
|
return GIT_USAGE;
|
|
56
87
|
if (args[0] !== 'push')
|
|
57
|
-
throw new Error(
|
|
88
|
+
throw new Error('only house git push [commit] exists; use plain git for the rest');
|
|
58
89
|
const owner = args.includes('--owner');
|
|
59
90
|
const commit = args.slice(1).find((arg) => arg !== '--owner') ?? 'HEAD';
|
|
60
91
|
if (owner && socketPath !== undefined)
|
|
61
|
-
throw new Error('
|
|
92
|
+
throw new Error('--owner is not for Agent conversations; run house git push without it');
|
|
62
93
|
if (!owner && socketPath === undefined)
|
|
63
94
|
throw new Error('outside an Agent conversation, house git push needs --owner');
|
|
64
95
|
const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
|
|
@@ -66,30 +97,45 @@ async function gitPush(args) {
|
|
|
66
97
|
throw new Error(pushed.text);
|
|
67
98
|
return pushed.text;
|
|
68
99
|
}
|
|
100
|
+
function argumentsOf(argument) {
|
|
101
|
+
if (argument === undefined)
|
|
102
|
+
return {};
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(argument);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error(ARGUMENTS);
|
|
109
|
+
}
|
|
110
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
111
|
+
throw new Error(ARGUMENTS);
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
69
114
|
async function house([verb, argument]) {
|
|
70
115
|
if (verb === undefined || verb === '--help') {
|
|
71
|
-
return [USAGE,
|
|
116
|
+
return [USAGE, 'git push [commit]', ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
|
|
72
117
|
}
|
|
73
118
|
if (argument === '--help') {
|
|
74
119
|
const tool = (await tools()).find((listed) => listed.name === verb);
|
|
75
120
|
if (tool === undefined)
|
|
76
|
-
throw new Error(
|
|
77
|
-
return `${tool.description}\n${JSON.stringify(tool.inputSchema
|
|
121
|
+
throw new Error(`${verb} is no Tool; house find_command '{"query":"${verb}"}' finds Commands`);
|
|
122
|
+
return `${tool.description}\n${JSON.stringify(tool.inputSchema)}`;
|
|
123
|
+
}
|
|
124
|
+
const args = argumentsOf(argument);
|
|
125
|
+
if (verb === 'upload_attachment') {
|
|
126
|
+
return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload_attachment needs "path", a local file') });
|
|
78
127
|
}
|
|
79
|
-
const args = (argument === undefined ? {} : JSON.parse(argument));
|
|
80
|
-
if (verb === 'upload_attachment')
|
|
81
|
-
return upload(String(args.path), args.room_ref);
|
|
82
128
|
if (verb === 'append_record' && Array.isArray(args.attachments)) {
|
|
83
129
|
const attachments = [];
|
|
84
130
|
for (const path of args.attachments) {
|
|
85
|
-
attachments.push(
|
|
131
|
+
attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
|
|
86
132
|
}
|
|
87
133
|
args.attachments = attachments;
|
|
88
134
|
}
|
|
89
135
|
const result = await mcp('tools/call', { name: verb, arguments: args });
|
|
90
136
|
const text = result.content.map((content) => content.text).join('\n');
|
|
91
137
|
if (result.isError === true)
|
|
92
|
-
throw new Error(text);
|
|
138
|
+
throw new Error(text.replace(/\n+$/, ''));
|
|
93
139
|
return text;
|
|
94
140
|
}
|
|
95
141
|
const argv = process.argv.slice(2);
|
package/dist/instructions.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { readEnrolment } from './home.js';
|
|
2
1
|
import { HOW_WE_WORK } from './skills.js';
|
|
3
2
|
const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
|
|
4
|
-
const FILE_LINE = 'To give the User a file, upload it with `house upload_attachment` and link it
|
|
5
|
-
const COPIES_LINE = '
|
|
3
|
+
const FILE_LINE = 'To give the User a file or a page, upload it with `house upload_attachment` and link it; never start a server.';
|
|
4
|
+
const COPIES_LINE = 'House working copies; commit, then `house git push`:';
|
|
6
5
|
const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
|
|
6
|
+
const FAILED = /^exit: [1-9]\d*$/;
|
|
7
7
|
const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
|
|
8
|
+
function cutOf(lines) {
|
|
9
|
+
const cut = CUT.exec(lines[0] ?? '');
|
|
10
|
+
return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
|
|
11
|
+
}
|
|
8
12
|
async function howWeWork(bridge) {
|
|
9
13
|
let document = '';
|
|
10
14
|
for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
|
|
11
15
|
const read = await bridge.tool('shell', { command });
|
|
12
16
|
const text = read.content.map((content) => content.text).join('\n');
|
|
13
17
|
const lines = text.split('\n');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const refusal = read.isError === true ? text : lines.slice(at).join('\n').replace(/^stderr: /gm, '');
|
|
18
|
+
if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
|
|
19
|
+
const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
|
|
17
20
|
if (UNREADABLE.test(refusal))
|
|
18
21
|
return '';
|
|
19
22
|
throw new Error(refusal);
|
|
20
23
|
}
|
|
21
|
-
const cut =
|
|
22
|
-
|
|
23
|
-
document += (cut === null ? shown : shown.subarray(0, Number(cut[1]))).toString();
|
|
24
|
+
const cut = cutOf(lines);
|
|
25
|
+
document += lines.slice(cut === null ? 0 : 1).join('\n');
|
|
24
26
|
command = cut?.[2];
|
|
25
27
|
}
|
|
26
28
|
return document.replace(/\n$/, '');
|
|
27
29
|
}
|
|
28
30
|
export async function instructions(bridge, base, copies) {
|
|
29
|
-
const
|
|
30
|
-
const appLine = `To show the User a page, upload it or push it to an App instead of starting a server; \`git clone ${app}\` starts a new App.`;
|
|
31
|
-
const lines = [HOUSE_LINE, FILE_LINE, appLine, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
|
|
31
|
+
const lines = [HOUSE_LINE, FILE_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
|
|
32
32
|
return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
|
|
33
33
|
}
|
package/dist/operation.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
export function uuidOf(bytes, version) {
|
|
3
|
+
bytes[6] = (version << 4) | (bytes[6] & 0x0f);
|
|
4
|
+
bytes[8] = 0x80 | (bytes[8] & 0x3f);
|
|
5
|
+
const hex = bytes.toString('hex', 0, 16);
|
|
6
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
7
|
+
}
|
|
2
8
|
export function operationId() {
|
|
3
9
|
const bytes = randomBytes(16);
|
|
4
10
|
bytes.writeUIntBE(Date.now(), 0, 6);
|
|
5
|
-
|
|
6
|
-
bytes[8] = 0x80 | (bytes[8] & 0x3f);
|
|
7
|
-
const hex = bytes.toString('hex');
|
|
8
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
11
|
+
return uuidOf(bytes, 7);
|
|
9
12
|
}
|
package/dist/refusals.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parse } from 'yaml';
|
|
2
|
+
const HEAD = /^([a-z][a-z0-9_]*): \S/;
|
|
3
|
+
function conflictCodes(detail) {
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
parsed = parse(detail);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const conflicts = parsed?.conflicts;
|
|
12
|
+
if (!Array.isArray(conflicts))
|
|
13
|
+
return [];
|
|
14
|
+
return conflicts.flatMap((conflict) => typeof conflict?.code === 'string' ? [conflict.code] : []);
|
|
15
|
+
}
|
|
16
|
+
function spoken(text) {
|
|
17
|
+
const head = HEAD.exec(text);
|
|
18
|
+
if (head === null)
|
|
19
|
+
return null;
|
|
20
|
+
const said = text.replace(/\n+$/, '');
|
|
21
|
+
const detail = said.indexOf('\n');
|
|
22
|
+
return { code: head[1], text: said, conflicts: detail === -1 ? [] : conflictCodes(said.slice(detail + 1)) };
|
|
23
|
+
}
|
|
24
|
+
export function refusalOf(text) {
|
|
25
|
+
let body;
|
|
26
|
+
try {
|
|
27
|
+
body = JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return spoken(text);
|
|
31
|
+
}
|
|
32
|
+
const error = body?.error;
|
|
33
|
+
if (typeof error !== 'object' || error === null)
|
|
34
|
+
return null;
|
|
35
|
+
const { code, message, data } = error;
|
|
36
|
+
const said = typeof message === 'string' ? spoken(message) : null;
|
|
37
|
+
if (said !== null)
|
|
38
|
+
return said;
|
|
39
|
+
const named = typeof code === 'string' ? code : typeof data?.code === 'string' ? data.code : null;
|
|
40
|
+
return named === null ? null : { code: named, text: named, conflicts: [] };
|
|
41
|
+
}
|
package/dist/resident.js
CHANGED
|
@@ -14,6 +14,7 @@ import { SignIns } from './sign-in.js';
|
|
|
14
14
|
import { placeSkillSet } from './skills.js';
|
|
15
15
|
import { holdStream } from './stream.js';
|
|
16
16
|
import { Updates } from './update.js';
|
|
17
|
+
const AGENTS_REREAD_MS = 60 * 60_000;
|
|
17
18
|
function report(error) {
|
|
18
19
|
process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
19
20
|
}
|
|
@@ -93,6 +94,7 @@ export async function resident() {
|
|
|
93
94
|
await house.deliver('/kit/restarted', {}).catch(report);
|
|
94
95
|
await updates.resumed().catch(report);
|
|
95
96
|
logged(agents.refresh());
|
|
97
|
+
setInterval(() => logged(agents.refresh()), AGENTS_REREAD_MS);
|
|
96
98
|
stream = holdStream({
|
|
97
99
|
opened: () => {
|
|
98
100
|
conversations.opened();
|
package/dist/skills.js
CHANGED
|
@@ -3,10 +3,9 @@ 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';
|
|
8
|
+
import { refusalOf } from './refusals.js';
|
|
10
9
|
export const HOW_WE_WORK = '/private/library/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');
|
|
@@ -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/how-we-work.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Room is where information people share is kept; it is not the place for
|
|
4
4
|
private information. Work goes into the Room it belongs to, or the First Room
|
|
5
|
-
when none is named.
|
|
5
|
+
when none is named.
|
|
6
6
|
|
|
7
7
|
The skills are already set up; never run `setup-matt-pocock-skills`.
|
|
8
8
|
|
|
@@ -20,6 +20,4 @@ The skills are already set up; never run `setup-matt-pocock-skills`.
|
|
|
20
20
|
- Research: `library/research/<slug>.md`
|
|
21
21
|
- Questionnaires: `library/questionnaires/<slug>.md`
|
|
22
22
|
- Learning: `library/learning/<topic>/`
|
|
23
|
-
-
|
|
24
|
-
linked from the document they belong to; a prototype is one HTML file with
|
|
25
|
-
every variant on one page.
|
|
23
|
+
- Prototype: one HTML file with every variant on one page.
|