@agentshouse/kit 0.2.1-alpha.8 → 0.2.1-c05cd91c26b1d

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 (150) hide show
  1. package/README.md +46 -21
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +48 -85
  4. package/dist/agents.js +312 -53
  5. package/dist/api.js +40 -4
  6. package/dist/app-git.js +48 -0
  7. package/dist/bootstrap.json +10 -14
  8. package/dist/bridge.js +92 -77
  9. package/dist/chatgpt.js +32 -0
  10. package/dist/clis-main.js +23 -0
  11. package/dist/clis.js +213 -71
  12. package/dist/codex-main.js +58 -0
  13. package/dist/configure-main.js +6 -0
  14. package/dist/conversations.js +429 -232
  15. package/dist/copies.js +233 -171
  16. package/dist/end.js +23 -0
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +525 -0
  19. package/dist/files.js +24 -3
  20. package/dist/git-main.js +2 -2
  21. package/dist/git.js +15 -9
  22. package/dist/home.js +141 -10
  23. package/dist/house-cli.js +291 -0
  24. package/dist/house-main.js +5 -102
  25. package/dist/inputs.js +37 -0
  26. package/dist/instructions.js +10 -31
  27. package/dist/kit-cli.js +68 -0
  28. package/dist/kit-main.js +8 -16
  29. package/dist/login.js +129 -94
  30. package/dist/logout.js +19 -0
  31. package/dist/mcp.js +39 -0
  32. package/dist/operation.js +7 -4
  33. package/dist/parts.js +82 -0
  34. package/dist/placement.js +30 -0
  35. package/dist/refusals.js +53 -0
  36. package/dist/relay.js +20 -0
  37. package/dist/resident.js +123 -40
  38. package/dist/scope.js +203 -0
  39. package/dist/secret-input.js +7 -2
  40. package/dist/service-main.js +9 -0
  41. package/dist/service.js +33 -0
  42. package/dist/shell.js +3 -0
  43. package/dist/sign-in.js +47 -28
  44. package/dist/skills.js +147 -0
  45. package/dist/stream.js +89 -13
  46. package/dist/transcription.js +170 -0
  47. package/dist/update.js +138 -0
  48. package/dist/wire.js +6 -0
  49. package/dist/work.js +346 -0
  50. package/how-we-work.md +33 -0
  51. package/package.json +8 -8
  52. package/skills/ask-matt/LICENSE +21 -0
  53. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  54. package/skills/ask-matt/SKILL.md +97 -0
  55. package/skills/ask-matt/agents/openai.yaml +5 -0
  56. package/skills/code-review/LICENSE +21 -0
  57. package/skills/code-review/SKILL.md +89 -0
  58. package/skills/code-review/agents/openai.yaml +3 -0
  59. package/skills/codebase-design/DEEPENING.md +37 -0
  60. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  61. package/skills/codebase-design/LICENSE +21 -0
  62. package/skills/codebase-design/SKILL.md +114 -0
  63. package/skills/codebase-design/agents/openai.yaml +3 -0
  64. package/skills/diagnosing-bugs/LICENSE +21 -0
  65. package/skills/diagnosing-bugs/SKILL.md +138 -0
  66. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  67. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  68. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  69. package/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  70. package/skills/domain-modeling/LICENSE +21 -0
  71. package/skills/domain-modeling/SKILL.md +74 -0
  72. package/skills/domain-modeling/agents/openai.yaml +3 -0
  73. package/skills/grill-me/LICENSE +21 -0
  74. package/skills/grill-me/SKILL.md +7 -0
  75. package/skills/grill-me/agents/openai.yaml +5 -0
  76. package/skills/grill-with-docs/LICENSE +21 -0
  77. package/skills/grill-with-docs/SKILL.md +7 -0
  78. package/skills/grill-with-docs/agents/openai.yaml +5 -0
  79. package/skills/grilling/LICENSE +21 -0
  80. package/skills/grilling/SKILL.md +30 -0
  81. package/skills/grilling/agents/openai.yaml +3 -0
  82. package/skills/handoff/LICENSE +21 -0
  83. package/skills/handoff/SKILL.md +16 -0
  84. package/skills/handoff/agents/openai.yaml +5 -0
  85. package/skills/implement/LICENSE +21 -0
  86. package/skills/implement/SKILL.md +17 -0
  87. package/skills/implement/agents/openai.yaml +5 -0
  88. package/skills/implement-spec/LICENSE +21 -0
  89. package/skills/implement-spec/SKILL.md +40 -0
  90. package/skills/implement-spec/agents/openai.yaml +5 -0
  91. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  92. package/skills/improve-codebase-architecture/LICENSE +21 -0
  93. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  94. package/skills/improve-codebase-architecture/agents/openai.yaml +5 -0
  95. package/skills/pr/CREDITS.md +3 -0
  96. package/skills/pr/LICENSE +21 -0
  97. package/skills/pr/SKILL.md +170 -0
  98. package/skills/pr/agents/openai.yaml +3 -0
  99. package/skills/prototype/LICENSE +21 -0
  100. package/skills/prototype/LOGIC.md +67 -0
  101. package/skills/prototype/SKILL.md +26 -0
  102. package/skills/prototype/UI.md +112 -0
  103. package/skills/prototype/agents/openai.yaml +3 -0
  104. package/skills/research/LICENSE +21 -0
  105. package/skills/research/SKILL.md +12 -0
  106. package/skills/research/agents/openai.yaml +3 -0
  107. package/skills/retro/LICENSE +21 -0
  108. package/skills/retro/SKILL.md +44 -0
  109. package/skills/retro/agents/openai.yaml +5 -0
  110. package/skills/tdd/LICENSE +21 -0
  111. package/skills/tdd/SKILL.md +38 -0
  112. package/skills/tdd/agents/openai.yaml +3 -0
  113. package/skills/tdd/mocking.md +59 -0
  114. package/skills/tdd/tests.md +77 -0
  115. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  116. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  117. package/skills/teach/LICENSE +21 -0
  118. package/skills/teach/MISSION-FORMAT.md +31 -0
  119. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  120. package/skills/teach/SKILL.md +140 -0
  121. package/skills/teach/agents/openai.yaml +5 -0
  122. package/skills/to-questionnaire/LICENSE +21 -0
  123. package/skills/to-questionnaire/SKILL.md +54 -0
  124. package/skills/to-questionnaire/agents/openai.yaml +5 -0
  125. package/skills/to-spec/LICENSE +21 -0
  126. package/skills/to-spec/SKILL.md +75 -0
  127. package/skills/to-spec/agents/openai.yaml +5 -0
  128. package/skills/to-tickets/LICENSE +21 -0
  129. package/skills/to-tickets/SKILL.md +105 -0
  130. package/skills/to-tickets/agents/openai.yaml +5 -0
  131. package/skills/triage/AGENT-BRIEF.md +207 -0
  132. package/skills/triage/LICENSE +21 -0
  133. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  134. package/skills/triage/SKILL.md +112 -0
  135. package/skills/triage/agents/openai.yaml +5 -0
  136. package/skills/wait-what/LICENSE +21 -0
  137. package/skills/wait-what/SKILL.md +7 -0
  138. package/skills/wait-what/agents/openai.yaml +5 -0
  139. package/skills/wayfinder/LICENSE +21 -0
  140. package/skills/wayfinder/SKILL.md +128 -0
  141. package/skills/wayfinder/agents/openai.yaml +5 -0
  142. package/skills/wizard/LICENSE +21 -0
  143. package/skills/wizard/SKILL.md +44 -0
  144. package/skills/wizard/agents/openai.yaml +3 -0
  145. package/skills/wizard/template.sh +223 -0
  146. package/skills/writing-for-agents/LICENSE +21 -0
  147. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  148. package/skills/writing-for-agents/SKILL.md +81 -0
  149. package/skills/writing-for-agents/agents/openai.yaml +3 -0
  150. package/dist/blocks.js +0 -51
package/dist/api.js CHANGED
@@ -1,4 +1,9 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { signInAgain, updateRequired } from './end.js';
1
3
  import { readEnrolment } from './home.js';
4
+ import { retryable } from './refusals.js';
5
+ import { relayed } from './relay.js';
6
+ import { KIT_WIRE, KIT_WIRE_HEADER, tooOld } from './wire.js';
2
7
  export class HouseRefusal extends Error {
3
8
  status;
4
9
  text;
@@ -8,24 +13,54 @@ export class HouseRefusal extends Error {
8
13
  this.text = text;
9
14
  }
10
15
  }
16
+ export function finalRefusal(error) {
17
+ return error instanceof HouseRefusal && retryable(error.text) === false;
18
+ }
19
+ export function retryDelay(attempt) {
20
+ // Full jitter, doubling from one second to a 30-second cap, spreads the Kits one outage dropped and retries within half a minute of House's return.
21
+ return Math.random() * Math.min(30_000, 1000 * 2 ** attempt);
22
+ }
23
+ export async function retrying(sent, signal, pause = retryDelay) {
24
+ for (let attempt = 0;; attempt++) {
25
+ try {
26
+ return await sent();
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof HouseRefusal) || retryable(error.text) !== true)
30
+ throw error;
31
+ }
32
+ // A request House refused as retryable is sent again after the caller's pause, the jittered curve unless it names its own.
33
+ await delay(pause(attempt), undefined, { signal });
34
+ }
35
+ }
11
36
  export async function post(house, path, body, credential, signal) {
12
- const answer = await fetch(new URL(path, house), {
37
+ const answer = await fetch(relayed(new URL(path, house)), {
13
38
  method: 'POST',
14
39
  headers: {
15
40
  'content-type': 'application/json',
41
+ [KIT_WIRE_HEADER]: String(KIT_WIRE),
16
42
  ...(credential === undefined ? {} : { authorization: `Bearer ${credential}` }),
17
43
  },
18
44
  body: JSON.stringify(body),
19
45
  signal,
20
46
  });
21
47
  const text = await answer.text();
48
+ if (!answer.ok && tooOld(text))
49
+ updateRequired();
22
50
  if (!answer.ok)
23
51
  throw new HouseRefusal(path, answer.status, text);
24
52
  return JSON.parse(text);
25
53
  }
26
54
  async function enrolledPost(path, body, signal) {
27
55
  const { house, credential } = (await readEnrolment());
28
- return post(house, path, body, credential, signal);
56
+ try {
57
+ return await post(house, path, body, credential, signal);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof HouseRefusal && error.status === 401)
61
+ await signInAgain(credential);
62
+ throw error;
63
+ }
29
64
  }
30
65
  export const house = {
31
66
  post: enrolledPost,
@@ -35,10 +70,11 @@ export const house = {
35
70
  return await enrolledPost(path, body);
36
71
  }
37
72
  catch (error) {
38
- if (error instanceof HouseRefusal && error.status < 500)
73
+ if (finalRefusal(error))
39
74
  throw error;
40
75
  process.stderr.write(`kit: ${path} did not reach House: ${error.message}\n`);
41
- await new Promise((resolve) => setTimeout(resolve, Math.min(30_000, 1000 * 2 ** attempt)));
76
+ // A delivery House did not take is sent again on the jittered curve for as long as it is still current.
77
+ await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt)));
42
78
  if (!current())
43
79
  throw error;
44
80
  }
@@ -0,0 +1,48 @@
1
+ import { run } from './git.js';
2
+ import { kitHome, readOwnAgent } from './home.js';
3
+ const HELPER = '!kit git-credential';
4
+ function section(house) {
5
+ return `credential.${new URL('/app/', house).href}`;
6
+ }
7
+ async function configured(args) {
8
+ try {
9
+ return await run(kitHome(), ['config', '--global', ...args]);
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT')
13
+ return null;
14
+ throw error;
15
+ }
16
+ }
17
+ export async function placeAppGit(ownAgent) {
18
+ if (ownAgent === null)
19
+ return;
20
+ await configured(['--remove-section', section(ownAgent.house)]);
21
+ for (const value of ['', HELPER]) {
22
+ const ran = await configured(['--add', `${section(ownAgent.house)}.helper`, value]);
23
+ if (ran === null)
24
+ return;
25
+ if (ran.status !== 0)
26
+ throw new Error(`git config failed: ${ran.stderr.trim()}`);
27
+ }
28
+ }
29
+ export async function answerGitCredential(operation) {
30
+ let asked = '';
31
+ for await (const chunk of process.stdin)
32
+ asked += String(chunk);
33
+ if (operation !== 'get')
34
+ return;
35
+ const attributes = new Map(asked
36
+ .split('\n')
37
+ .filter((line) => line.includes('='))
38
+ .map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]));
39
+ const ownAgent = await readOwnAgent();
40
+ if (ownAgent === null) {
41
+ process.stderr.write(`kit: Kit home ${kitHome()} is not signed in; run kit login\n`);
42
+ return;
43
+ }
44
+ const house = new URL(ownAgent.house);
45
+ if (attributes.get('protocol') !== house.protocol.slice(0, -1) || attributes.get('host') !== house.host)
46
+ return;
47
+ process.stdout.write(`username=house\npassword=${ownAgent.credential}\n`);
48
+ }
@@ -1,27 +1,23 @@
1
1
  {
2
- "schemaVersion": 1,
3
- "version": "0.2.1-alpha.8",
2
+ "schemaVersion": 2,
3
+ "version": "0.2.1-c05cd91c26b1d",
4
4
  "hosts": {
5
+ "macos": {
6
+ "asset": {
7
+ "name": "connect-linux.sh",
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c05cd91c26b1d/connect-linux.sh"
9
+ }
10
+ },
5
11
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.8/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.8/connect-linux.sh"
10
- },
11
- "workspace": {
12
- "argument": "--workspace",
13
- "quoting": "posix"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c05cd91c26b1d/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.8/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.8/connect-windows.ps1"
21
- },
22
- "workspace": {
23
- "argument": "--workspace",
24
- "quoting": "powershell"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c05cd91c26b1d/connect-windows.ps1"
25
21
  }
26
22
  }
27
23
  }
package/dist/bridge.js CHANGED
@@ -1,17 +1,19 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
- import { mkdir } from 'node:fs/promises';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
4
  import { createServer, request as httpRequest } from 'node:http';
5
5
  import { request as httpsRequest } from 'node:https';
6
- import { delimiter, join } from 'node:path';
6
+ import { delimiter, dirname, join } from 'node:path';
7
7
  import { parse } from 'yaml';
8
- import { KIT_VERSION } from './clis.js';
9
- import { changedPaths, refusalCode, } from './copies.js';
10
- import { kitHome, readEnrolment } from './home.js';
8
+ import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
9
+ import { loginPath } from './clis.js';
10
+ import { changedPaths, } from './copies.js';
11
+ import { kitHome, readEnrolment, socketDirectory } from './home.js';
12
+ import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
11
13
  import { operationId } from './operation.js';
12
- const MCP_PROTOCOL_VERSION = '2026-07-28';
14
+ import { refusalOf, retryable } from './refusals.js';
15
+ import { relayed } from './relay.js';
13
16
  const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
14
- const FORWARDED = new Set(['/', '/kit/attachments/upload']);
15
17
  export async function received(request) {
16
18
  let text = '';
17
19
  request.setEncoding('utf8');
@@ -19,7 +21,8 @@ export async function received(request) {
19
21
  text += chunk;
20
22
  return text;
21
23
  }
22
- function relay(request, response, target, authorization) {
24
+ function relay(request, response, house, authorization) {
25
+ const target = relayed(house);
23
26
  const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
24
27
  const forwarded = send(target, {
25
28
  method: request.method,
@@ -31,16 +34,37 @@ function relay(request, response, target, authorization) {
31
34
  forwarded.on('error', () => response.destroy());
32
35
  request.pipe(forwarded);
33
36
  }
37
+ function refusalIn(text, status) {
38
+ return refusalOf(text) ?? { code: '', text: text === '' ? `House refused it with ${status}` : text, conflicts: [] };
39
+ }
34
40
  function answered(forwarded) {
35
- if (forwarded.status >= 500)
36
- throw new Error(`House answered ${forwarded.status}`);
37
41
  if (forwarded.status !== 200)
38
- return { refused: { code: refusalCode(forwarded.text), text: forwarded.text } };
42
+ return { refused: refusalIn(forwarded.text, forwarded.status) };
39
43
  const answer = JSON.parse(forwarded.text);
40
44
  if (answer.error !== undefined)
41
- return { refused: { code: refusalCode(answer.error.message), text: answer.error.message } };
45
+ return { refused: refusalIn(answer.error.message, forwarded.status) };
42
46
  const text = answer.result.content.map((content) => content.text).join('\n');
43
- return answer.result.isError === true ? { refused: { code: refusalCode(text), text } } : { text };
47
+ return answer.result.isError === true ? { refused: refusalIn(text, forwarded.status) } : { text };
48
+ }
49
+ function pendingSeconds(forwarded) {
50
+ if (forwarded.status !== 200)
51
+ return null;
52
+ const { result } = JSON.parse(forwarded.text);
53
+ if (result === undefined || result.isError === true)
54
+ return null;
55
+ const text = result.content.map((content) => content.text).join('\n');
56
+ if (!text.startsWith('{"pending":'))
57
+ return null;
58
+ const seconds = JSON.parse(text).pending.retry_after;
59
+ return typeof seconds === 'number' ? seconds : null;
60
+ }
61
+ function resendAfter(forwarded, attempt) {
62
+ if (forwarded === null)
63
+ return retryDelay(attempt);
64
+ if (forwarded.status !== 200)
65
+ return retryable(forwarded.text) === false ? null : retryDelay(attempt);
66
+ const seconds = pendingSeconds(forwarded);
67
+ return seconds === null ? null : seconds * 1000;
44
68
  }
45
69
  function editAnswer(forwarded) {
46
70
  const answer = answered(forwarded);
@@ -50,44 +74,33 @@ function editAnswer(forwarded) {
50
74
  return {
51
75
  accepted: results.flatMap((result) => [
52
76
  ...(result.from === undefined ? [] : [{ path: result.from, revision: null, content: null }]),
53
- { path: result.path, revision: result.op === 'remove' ? null : result.revision },
77
+ { path: result.path, revision: result.revision ?? null },
54
78
  ]),
55
79
  };
56
80
  }
57
81
  export async function openBridge(house, copies, conversationId, signal) {
82
+ const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
58
83
  const origin = (await readEnrolment()).house;
59
- const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
84
+ const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
60
85
  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
- });
86
+ const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
73
87
  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() };
88
+ if (operation === null)
89
+ return sent(message, null);
90
+ for (let attempt = 0;; attempt++) {
91
+ const forwarded = await sent(message, operation).catch((error) => {
92
+ if (closed.signal.aborted)
93
+ throw error;
94
+ return null;
95
+ });
96
+ const after = resendAfter(forwarded, attempt);
97
+ if (after === null)
98
+ return forwarded;
99
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
100
+ await delay(after, undefined, { signal: closed.signal });
101
+ }
89
102
  };
90
- const call = (name, args) => ({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } });
103
+ const call = (name, args) => rpc('tools/call', { name, arguments: args });
91
104
  const staged = async (operation, changes) => {
92
105
  const bytes = Buffer.from(JSON.stringify({ changes }));
93
106
  const sha256 = createHash('sha256').update(bytes).digest('hex');
@@ -98,42 +111,39 @@ export async function openBridge(house, copies, conversationId, signal) {
98
111
  if ('refused' in prepared)
99
112
  return prepared;
100
113
  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 } };
114
+ let text;
115
+ try {
116
+ text = await retrying(async () => {
117
+ const posted = await fetch(relayed(transfer.url), {
118
+ method: 'POST',
119
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
120
+ body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
121
+ signal: closed.signal,
122
+ });
123
+ const answer = await posted.text();
124
+ if (!posted.ok)
125
+ throw new HouseRefusal('/', posted.status, answer);
126
+ return answer;
127
+ }, closed.signal);
128
+ }
129
+ catch (error) {
130
+ if (!finalRefusal(error))
131
+ throw error;
132
+ return { refused: refusalIn(error.text, error.status) };
133
+ }
112
134
  const held = JSON.parse(text).transfer;
113
135
  return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
114
136
  };
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() };
137
+ const forward = (text) => {
138
+ const message = JSON.parse(text);
139
+ return send(message, message.method === 'tools/call' ? operationId() : null);
130
140
  };
131
141
  const caller = {
132
142
  key: `conversation:${conversationId}`,
133
143
  submit: async (operation, changes, held, retain) => {
134
144
  const inline = call('edit', { changes });
135
145
  let upload = held;
136
- if (upload === undefined && Buffer.byteLength(body(inline, operation)) > MCP_REQUEST_BYTES) {
146
+ if (upload === undefined && Buffer.byteLength(mcpBody(inline, operation)) > MCP_REQUEST_BYTES) {
137
147
  const prepared = await staged(operation, changes);
138
148
  if ('refused' in prepared)
139
149
  return prepared;
@@ -148,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
148
158
  const { cwd, commit } = JSON.parse(text);
149
159
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
150
160
  }
161
+ if (path === '/git/fetch')
162
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
151
163
  if (path === '/git/observed') {
152
164
  await copies.observed(JSON.parse(text));
153
165
  return { status: 200, text: '{}' };
154
166
  }
155
- return FORWARDED.has(path) ? forward(path, text) : { status: 404, text: '' };
167
+ return path === '/' ? forward(text) : { status: 404, text: '' };
156
168
  };
157
- const sockets = join(kitHome(), 'bridges');
158
- await mkdir(sockets, { recursive: true, mode: 0o700 });
159
- const socketPath = join(sockets, `${randomUUID()}.sock`);
169
+ const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
160
170
  const bridge = createServer(async (request, response) => {
161
171
  try {
162
172
  const path = request.url ?? '';
@@ -168,7 +178,12 @@ export async function openBridge(house, copies, conversationId, signal) {
168
178
  response.writeHead(answered.status, { 'content-type': 'application/json' }).end(answered.text);
169
179
  }
170
180
  catch (error) {
171
- response.writeHead(502).end(error instanceof Error ? error.message : String(error));
181
+ if (error instanceof HouseRefusal) {
182
+ response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
183
+ return;
184
+ }
185
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
186
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
172
187
  }
173
188
  });
174
189
  bridge.listen(socketPath);
@@ -189,13 +204,13 @@ export async function openBridge(house, copies, conversationId, signal) {
189
204
  return {
190
205
  env: {
191
206
  HOUSE_BRIDGE: socketPath,
192
- PATH: `${join(kitHome(), 'shim')}${delimiter}${process.env.PATH ?? ''}`,
207
+ PATH: path,
193
208
  GIT_CONFIG_COUNT: '1',
194
209
  GIT_CONFIG_KEY_0: `url.${proxy}/app/.insteadOf`,
195
210
  GIT_CONFIG_VALUE_0: app,
196
211
  },
197
212
  tool: async (name, args) => {
198
- const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } }));
213
+ const forwarded = await forward(JSON.stringify(call(name, args)));
199
214
  if (forwarded.status !== 200)
200
215
  throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
201
216
  const answer = JSON.parse(forwarded.text);
@@ -0,0 +1,32 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { placeAppGit } from './app-git.js';
3
+ import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
4
+ import { accepted, DEFAULT_HOUSE } from './login.js';
5
+ const KEY_SOURCE = 'New key for a script on Connections';
6
+ const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
7
+ --key <key> a key from ${KEY_SOURCE}
8
+ --house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
9
+ `;
10
+ export async function chatgpt(argv) {
11
+ const { values } = parseArgs({
12
+ args: argv,
13
+ options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
14
+ });
15
+ if (values.help) {
16
+ process.stdout.write(USAGE);
17
+ return;
18
+ }
19
+ if (values.key === undefined)
20
+ throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
21
+ const before = await readOwnAgent();
22
+ const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
23
+ if (!(await accepted(house, values.key)))
24
+ throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
25
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
26
+ await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
27
+ await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
28
+ if (before !== null && before.credential !== values.key) {
29
+ process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
30
+ }
31
+ process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
32
+ }
@@ -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
+ }