@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c073f4e9391b2

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.
Files changed (48) hide show
  1. package/README.md +44 -26
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +18 -75
  4. package/dist/agents.js +266 -53
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +64 -0
  7. package/dist/bootstrap.json +10 -14
  8. package/dist/bridge.js +89 -71
  9. package/dist/chatgpt.js +25 -0
  10. package/dist/clis-main.js +23 -0
  11. package/dist/clis.js +203 -71
  12. package/dist/codex-main.js +58 -0
  13. package/dist/conversations.js +328 -199
  14. package/dist/copies.js +188 -161
  15. package/dist/end.js +22 -0
  16. package/dist/enrol-main.js +7 -1
  17. package/dist/events.js +521 -0
  18. package/dist/files.js +16 -1
  19. package/dist/git-main.js +2 -2
  20. package/dist/git.js +6 -2
  21. package/dist/home.js +72 -5
  22. package/dist/house-main.js +181 -57
  23. package/dist/instructions.js +16 -13
  24. package/dist/kit-main.js +55 -4
  25. package/dist/login.js +108 -97
  26. package/dist/logout.js +21 -0
  27. package/dist/mcp.js +39 -0
  28. package/dist/operation.js +7 -4
  29. package/dist/parts.js +82 -0
  30. package/dist/placement.js +48 -0
  31. package/dist/refusals.js +53 -0
  32. package/dist/relay.js +20 -0
  33. package/dist/resident.js +70 -34
  34. package/dist/scope.js +203 -0
  35. package/dist/secret-input.js +7 -2
  36. package/dist/service-main.js +9 -0
  37. package/dist/service.js +33 -0
  38. package/dist/shell.js +3 -0
  39. package/dist/sign-in.js +48 -11
  40. package/dist/skills.js +5 -10
  41. package/dist/stream.js +37 -7
  42. package/dist/transcription.js +170 -0
  43. package/dist/update.js +140 -0
  44. package/dist/wire.js +6 -0
  45. package/dist/work.js +348 -0
  46. package/how-we-work.md +10 -12
  47. package/package.json +4 -7
  48. package/dist/blocks.js +0 -51
@@ -0,0 +1,64 @@
1
+ import { extname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { run } from './git.js';
4
+ import { kitHome, readOwnAgent } from './home.js';
5
+ import { quoted } from './shell.js';
6
+ function section(house) {
7
+ return `credential.${new URL('/app/', house).href}`;
8
+ }
9
+ function helper() {
10
+ const main = fileURLToPath(new URL(`./kit-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
11
+ return `!HOUSE_KIT_HOME=${quoted(resolve(kitHome()))} ${quoted(process.execPath)} ${quoted(main)} git-credential`;
12
+ }
13
+ async function configured(args) {
14
+ try {
15
+ return await run(kitHome(), ['config', '--global', ...args]);
16
+ }
17
+ catch (error) {
18
+ if (error.code === 'ENOENT')
19
+ return null;
20
+ throw error;
21
+ }
22
+ }
23
+ async function forget(house) {
24
+ await configured(['--remove-section', section(house)]);
25
+ }
26
+ async function register(house) {
27
+ await forget(house);
28
+ for (const value of ['', helper()]) {
29
+ const ran = await configured(['--add', `${section(house)}.helper`, value]);
30
+ if (ran === null)
31
+ return;
32
+ if (ran.status !== 0)
33
+ throw new Error(`git config failed: ${ran.stderr.trim()}`);
34
+ }
35
+ }
36
+ export async function placeAppGit(before, after) {
37
+ if (before !== null && before.house !== after?.house)
38
+ await forget(before.house);
39
+ if (after !== null)
40
+ await register(after.house);
41
+ }
42
+ export async function forgetAppGit() {
43
+ const ownAgent = await readOwnAgent();
44
+ if (ownAgent !== null)
45
+ await forget(ownAgent.house);
46
+ }
47
+ export async function answerGitCredential(operation) {
48
+ let asked = '';
49
+ for await (const chunk of process.stdin)
50
+ asked += String(chunk);
51
+ if (operation !== 'get')
52
+ return;
53
+ const attributes = new Map(asked
54
+ .split('\n')
55
+ .filter((line) => line.includes('='))
56
+ .map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
57
+ const ownAgent = await readOwnAgent();
58
+ if (ownAgent === null)
59
+ return;
60
+ const house = new URL(ownAgent.house);
61
+ if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
62
+ return;
63
+ process.stdout.write(`username=house\npassword=${ownAgent.credential}\n`);
64
+ }
@@ -1,27 +1,23 @@
1
1
  {
2
- "schemaVersion": 1,
3
- "version": "0.2.1-alpha.9",
2
+ "schemaVersion": 2,
3
+ "version": "0.2.1-c073f4e9391b2",
4
4
  "hosts": {
5
+ "macos": {
6
+ "asset": {
7
+ "name": "connect-linux.sh",
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c073f4e9391b2/connect-linux.sh"
9
+ }
10
+ },
5
11
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.9/connect-linux.sh)",
7
12
  "asset": {
8
13
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.9/connect-linux.sh"
10
- },
11
- "workspace": {
12
- "argument": "--workspace",
13
- "quoting": "posix"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c073f4e9391b2/connect-linux.sh"
14
15
  }
15
16
  },
16
17
  "windows": {
17
- "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.9/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.9/connect-windows.ps1"
21
- },
22
- "workspace": {
23
- "argument": "--workspace",
24
- "quoting": "powershell"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c073f4e9391b2/connect-windows.ps1"
25
21
  }
26
22
  }
27
23
  }
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
- import { delimiter, join } from 'node:path';
7
+ import { delimiter, dirname, join } from 'node:path';
7
8
  import { parse } from 'yaml';
8
- import { KIT_VERSION } from './clis.js';
9
- import { changedPaths, refusalCode, } from './copies.js';
9
+ import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
+ import { loginPath } from './clis.js';
11
+ import { changedPaths, } from './copies.js';
10
12
  import { kitHome, readEnrolment } from './home.js';
13
+ import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
11
14
  import { operationId } from './operation.js';
12
- const MCP_PROTOCOL_VERSION = '2026-07-28';
15
+ import { refusalOf, retryable } from './refusals.js';
16
+ import { relayed } from './relay.js';
13
17
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
14
- const FORWARDED = new Set(['/', '/kit/attachments/upload']);
15
18
  export async function received(request) {
16
19
  let text = '';
17
20
  request.setEncoding('utf8');
@@ -19,7 +22,8 @@ export async function received(request) {
19
22
  text += chunk;
20
23
  return text;
21
24
  }
22
- function relay(request, response, target, authorization) {
25
+ function relay(request, response, house, authorization) {
26
+ const target = relayed(house);
23
27
  const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
24
28
  const forwarded = send(target, {
25
29
  method: request.method,
@@ -31,16 +35,37 @@ function relay(request, response, target, authorization) {
31
35
  forwarded.on('error', () => response.destroy());
32
36
  request.pipe(forwarded);
33
37
  }
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
- if (forwarded.status >= 500)
36
- throw new Error(`House answered ${forwarded.status}`);
37
42
  if (forwarded.status !== 200)
38
- return { refused: { code: refusalCode(forwarded.text), text: forwarded.text } };
43
+ return { refused: refusalIn(forwarded.text, forwarded.status) };
39
44
  const answer = JSON.parse(forwarded.text);
40
45
  if (answer.error !== undefined)
41
- return { refused: { code: refusalCode(answer.error.message), text: answer.error.message } };
46
+ return { refused: refusalIn(answer.error.message, forwarded.status) };
42
47
  const text = answer.result.content.map((content) => content.text).join('\n');
43
- return answer.result.isError === true ? { refused: { code: refusalCode(text), text } } : { text };
48
+ return answer.result.isError === true ? { refused: refusalIn(text, forwarded.status) } : { text };
49
+ }
50
+ function pendingSeconds(forwarded) {
51
+ if (forwarded.status !== 200)
52
+ return null;
53
+ const { result } = JSON.parse(forwarded.text);
54
+ if (result === undefined || result.isError === true)
55
+ return null;
56
+ const text = result.content.map((content) => content.text).join('\n');
57
+ if (!text.startsWith('{"pending":'))
58
+ return null;
59
+ const seconds = JSON.parse(text).pending.retry_after;
60
+ return typeof seconds === 'number' ? seconds : null;
61
+ }
62
+ function resendAfter(forwarded, attempt) {
63
+ if (forwarded === null)
64
+ return retryDelay(attempt);
65
+ if (forwarded.status !== 200)
66
+ return retryable(forwarded.text) === false ? null : retryDelay(attempt);
67
+ const seconds = pendingSeconds(forwarded);
68
+ return seconds === null ? null : seconds * 1000;
44
69
  }
45
70
  function editAnswer(forwarded) {
46
71
  const answer = answered(forwarded);
@@ -50,44 +75,33 @@ function editAnswer(forwarded) {
50
75
  return {
51
76
  accepted: results.flatMap((result) => [
52
77
  ...(result.from === undefined ? [] : [{ path: result.from, revision: null, content: null }]),
53
- { path: result.path, revision: result.op === 'remove' ? null : result.revision },
78
+ { path: result.path, revision: result.revision ?? null },
54
79
  ]),
55
80
  };
56
81
  }
57
82
  export async function openBridge(house, copies, conversationId, signal) {
83
+ const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
58
84
  const origin = (await readEnrolment()).house;
59
85
  const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
60
86
  const closed = new AbortController();
61
- const body = (message, operation) => JSON.stringify({
62
- ...message,
63
- params: {
64
- ...message.params,
65
- _meta: {
66
- 'io.modelcontextprotocol/protocolVersion': MCP_PROTOCOL_VERSION,
67
- 'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version: KIT_VERSION },
68
- 'io.modelcontextprotocol/clientCapabilities': {},
69
- ...(operation === null ? {} : { 'agents.house/agent-operation': operation }),
70
- },
71
- },
72
- });
87
+ const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
73
88
  const send = async (message, operation) => {
74
- const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
75
- const answer = await fetch(new URL('/', origin), {
76
- method: 'POST',
77
- headers: {
78
- 'content-type': 'application/json',
79
- authorization: `Bearer ${credential}`,
80
- accept: 'application/json, text/event-stream',
81
- 'mcp-protocol-version': MCP_PROTOCOL_VERSION,
82
- 'mcp-method': message.method,
83
- ...(tool === null ? {} : { 'mcp-name': tool }),
84
- },
85
- body: body(message, operation),
86
- signal: closed.signal,
87
- });
88
- return { status: answer.status, text: await answer.text() };
89
+ if (operation === null)
90
+ return sent(message, null);
91
+ for (let attempt = 0;; attempt++) {
92
+ const forwarded = await sent(message, operation).catch((error) => {
93
+ if (closed.signal.aborted)
94
+ throw error;
95
+ return null;
96
+ });
97
+ const after = resendAfter(forwarded, attempt);
98
+ if (after === null)
99
+ return forwarded;
100
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
101
+ await delay(after, undefined, { signal: closed.signal });
102
+ }
89
103
  };
90
- const call = (name, args) => ({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } });
104
+ const call = (name, args) => rpc('tools/call', { name, arguments: args });
91
105
  const staged = async (operation, changes) => {
92
106
  const bytes = Buffer.from(JSON.stringify({ changes }));
93
107
  const sha256 = createHash('sha256').update(bytes).digest('hex');
@@ -98,42 +112,39 @@ export async function openBridge(house, copies, conversationId, signal) {
98
112
  if ('refused' in prepared)
99
113
  return prepared;
100
114
  const { transfer } = parse(prepared.text);
101
- const posted = await fetch(transfer.url, {
102
- method: 'POST',
103
- headers: { 'content-type': 'text/plain; charset=utf-8' },
104
- body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
105
- signal: closed.signal,
106
- });
107
- const text = await posted.text();
108
- if (posted.status >= 500)
109
- throw new Error(`House answered ${posted.status}`);
110
- if (!posted.ok)
111
- return { refused: { code: refusalCode(text), text } };
115
+ let text;
116
+ try {
117
+ text = await retrying(async () => {
118
+ const posted = await fetch(relayed(transfer.url), {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
121
+ body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
122
+ signal: closed.signal,
123
+ });
124
+ const answer = await posted.text();
125
+ if (!posted.ok)
126
+ throw new HouseRefusal('/', posted.status, answer);
127
+ return answer;
128
+ }, closed.signal);
129
+ }
130
+ catch (error) {
131
+ if (!finalRefusal(error))
132
+ throw error;
133
+ return { refused: refusalIn(error.text, error.status) };
134
+ }
112
135
  const held = JSON.parse(text).transfer;
113
136
  return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
114
137
  };
115
- const forward = async (path, text) => {
116
- if (path === '/') {
117
- const message = JSON.parse(text);
118
- const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
119
- const writes = tool !== null &&
120
- (await house.post('/kit/tools/mutations', {}, closed.signal)).tools.some((listed) => listed.name === tool && listed.writes);
121
- return send(message, writes ? operationId() : null);
122
- }
123
- const answer = await fetch(new URL(path, origin), {
124
- method: 'POST',
125
- headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
126
- body: text,
127
- signal: closed.signal,
128
- });
129
- return { status: answer.status, text: await answer.text() };
138
+ const forward = (text) => {
139
+ const message = JSON.parse(text);
140
+ return send(message, message.method === 'tools/call' ? operationId() : null);
130
141
  };
131
142
  const caller = {
132
143
  key: `conversation:${conversationId}`,
133
144
  submit: async (operation, changes, held, retain) => {
134
145
  const inline = call('edit', { changes });
135
146
  let upload = held;
136
- if (upload === undefined && Buffer.byteLength(body(inline, operation)) > MCP_REQUEST_BYTES) {
147
+ if (upload === undefined && Buffer.byteLength(mcpBody(inline, operation)) > MCP_REQUEST_BYTES) {
137
148
  const prepared = await staged(operation, changes);
138
149
  if ('refused' in prepared)
139
150
  return prepared;
@@ -148,11 +159,13 @@ export async function openBridge(house, copies, conversationId, signal) {
148
159
  const { cwd, commit } = JSON.parse(text);
149
160
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
150
161
  }
162
+ if (path === '/git/fetch')
163
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
151
164
  if (path === '/git/observed') {
152
165
  await copies.observed(JSON.parse(text));
153
166
  return { status: 200, text: '{}' };
154
167
  }
155
- return FORWARDED.has(path) ? forward(path, text) : { status: 404, text: '' };
168
+ return path === '/' ? forward(text) : { status: 404, text: '' };
156
169
  };
157
170
  const sockets = join(kitHome(), 'bridges');
158
171
  await mkdir(sockets, { recursive: true, mode: 0o700 });
@@ -168,7 +181,12 @@ export async function openBridge(house, copies, conversationId, signal) {
168
181
  response.writeHead(answered.status, { 'content-type': 'application/json' }).end(answered.text);
169
182
  }
170
183
  catch (error) {
171
- response.writeHead(502).end(error instanceof Error ? error.message : String(error));
184
+ if (error instanceof HouseRefusal) {
185
+ response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
186
+ return;
187
+ }
188
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
189
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
172
190
  }
173
191
  });
174
192
  bridge.listen(socketPath);
@@ -189,13 +207,13 @@ export async function openBridge(house, copies, conversationId, signal) {
189
207
  return {
190
208
  env: {
191
209
  HOUSE_BRIDGE: socketPath,
192
- PATH: `${join(kitHome(), 'shim')}${delimiter}${process.env.PATH ?? ''}`,
210
+ PATH: path,
193
211
  GIT_CONFIG_COUNT: '1',
194
212
  GIT_CONFIG_KEY_0: `url.${proxy}/app/.insteadOf`,
195
213
  GIT_CONFIG_VALUE_0: app,
196
214
  },
197
215
  tool: async (name, args) => {
198
- const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } }));
216
+ const forwarded = await forward(JSON.stringify(call(name, args)));
199
217
  if (forwarded.status !== 200)
200
218
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
201
219
  const answer = JSON.parse(forwarded.text);
@@ -0,0 +1,25 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
3
+ import { accepted, DEFAULT_HOUSE } from './login.js';
4
+ const KEY_SOURCE = 'New key for a script on Connections';
5
+ const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
6
+ --key <key> a key from ${KEY_SOURCE}
7
+ --house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
8
+ `;
9
+ export async function chatgpt(argv) {
10
+ const { values } = parseArgs({
11
+ args: argv,
12
+ options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
13
+ });
14
+ if (values.help) {
15
+ process.stdout.write(USAGE);
16
+ return;
17
+ }
18
+ if (values.key === undefined)
19
+ throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
20
+ const house = values.house ?? (await readOwnAgent())?.house ?? DEFAULT_HOUSE;
21
+ if (!(await accepted(house, values.key)))
22
+ throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
23
+ await writeOwnAgent({ house, credential: values.key });
24
+ process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
25
+ }
@@ -0,0 +1,23 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ import { post } from './api.js';
3
+ import { CLIS, below, locate, versionOf } from './clis.js';
4
+ import { readEnrolment } from './home.js';
5
+ setGlobalProxyFromEnv();
6
+ try {
7
+ const { house, credential } = (await readEnrolment());
8
+ const { agents } = await post(house, '/kit/agents/desired', {}, credential);
9
+ for (const kind of agents) {
10
+ const cli = await locate(kind);
11
+ if (cli === null)
12
+ continue;
13
+ const release = await versionOf(cli);
14
+ if (release === null)
15
+ continue;
16
+ const { bin, minimum } = CLIS[kind];
17
+ process.stdout.write(`${[bin, cli, release, minimum, below(release, minimum) ? 'old' : 'current'].join('\t')}\n`);
18
+ }
19
+ }
20
+ catch (error) {
21
+ process.stderr.write(`kit: ${error.message}\n`);
22
+ process.exitCode = 1;
23
+ }