@agentshouse/kit 0.2.0 → 0.2.1-alpha.10

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 (113) hide show
  1. package/README.md +20 -2
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +96 -0
  4. package/dist/api.js +39 -3
  5. package/dist/authority-main.js +22 -0
  6. package/dist/blocks.js +51 -0
  7. package/dist/bootstrap.json +28 -0
  8. package/dist/bridge.js +214 -0
  9. package/dist/clis.js +114 -0
  10. package/dist/configure-main.js +6 -0
  11. package/dist/conversations.js +561 -0
  12. package/dist/copies.js +796 -0
  13. package/dist/enrol-main.js +4 -0
  14. package/dist/files.js +21 -0
  15. package/dist/git-main.js +153 -0
  16. package/dist/git.js +81 -0
  17. package/dist/home.js +19 -6
  18. package/dist/house-main.js +106 -3
  19. package/dist/instructions.js +33 -0
  20. package/dist/kit-main.js +2 -0
  21. package/dist/login.js +6 -2
  22. package/dist/operation.js +9 -0
  23. package/dist/resident.js +113 -2
  24. package/dist/secret-input.js +16 -0
  25. package/dist/sign-in.js +87 -0
  26. package/dist/skills.js +83 -0
  27. package/dist/stream.js +17 -1
  28. package/dist/translate.js +51 -0
  29. package/dist/update.js +116 -0
  30. package/how-we-work.md +25 -0
  31. package/package.json +29 -3
  32. package/skills/ask-matt/LICENSE +21 -0
  33. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  34. package/skills/ask-matt/SKILL.md +91 -0
  35. package/skills/ask-matt/agents/openai.yaml +4 -0
  36. package/skills/code-review/LICENSE +21 -0
  37. package/skills/code-review/SKILL.md +89 -0
  38. package/skills/code-review/agents/openai.yaml +3 -0
  39. package/skills/codebase-design/DEEPENING.md +37 -0
  40. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  41. package/skills/codebase-design/LICENSE +21 -0
  42. package/skills/codebase-design/SKILL.md +116 -0
  43. package/skills/codebase-design/agents/openai.yaml +3 -0
  44. package/skills/diagnosing-bugs/LICENSE +21 -0
  45. package/skills/diagnosing-bugs/SKILL.md +140 -0
  46. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  47. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  48. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  49. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  50. package/skills/domain-modeling/LICENSE +21 -0
  51. package/skills/domain-modeling/SKILL.md +76 -0
  52. package/skills/domain-modeling/agents/openai.yaml +3 -0
  53. package/skills/grill-me/LICENSE +21 -0
  54. package/skills/grill-me/SKILL.md +8 -0
  55. package/skills/grill-me/agents/openai.yaml +4 -0
  56. package/skills/grill-with-docs/LICENSE +21 -0
  57. package/skills/grill-with-docs/SKILL.md +8 -0
  58. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  59. package/skills/grilling/LICENSE +21 -0
  60. package/skills/grilling/SKILL.md +30 -0
  61. package/skills/grilling/agents/openai.yaml +3 -0
  62. package/skills/handoff/LICENSE +21 -0
  63. package/skills/handoff/SKILL.md +17 -0
  64. package/skills/handoff/agents/openai.yaml +4 -0
  65. package/skills/implement/LICENSE +21 -0
  66. package/skills/implement/SKILL.md +16 -0
  67. package/skills/implement/agents/openai.yaml +4 -0
  68. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  69. package/skills/improve-codebase-architecture/LICENSE +21 -0
  70. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  71. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  72. package/skills/prototype/LICENSE +21 -0
  73. package/skills/prototype/LOGIC.md +67 -0
  74. package/skills/prototype/SKILL.md +28 -0
  75. package/skills/prototype/UI.md +112 -0
  76. package/skills/prototype/agents/openai.yaml +3 -0
  77. package/skills/research/LICENSE +21 -0
  78. package/skills/research/SKILL.md +14 -0
  79. package/skills/research/agents/openai.yaml +3 -0
  80. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  81. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  82. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  83. package/skills/tdd/LICENSE +21 -0
  84. package/skills/tdd/SKILL.md +40 -0
  85. package/skills/tdd/agents/openai.yaml +3 -0
  86. package/skills/tdd/mocking.md +59 -0
  87. package/skills/tdd/tests.md +77 -0
  88. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  89. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  90. package/skills/teach/LICENSE +21 -0
  91. package/skills/teach/MISSION-FORMAT.md +31 -0
  92. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  93. package/skills/teach/SKILL.md +141 -0
  94. package/skills/teach/agents/openai.yaml +4 -0
  95. package/skills/to-questionnaire/LICENSE +21 -0
  96. package/skills/to-questionnaire/SKILL.md +55 -0
  97. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  98. package/skills/to-spec/LICENSE +21 -0
  99. package/skills/to-spec/SKILL.md +76 -0
  100. package/skills/to-spec/agents/openai.yaml +4 -0
  101. package/skills/to-tickets/LICENSE +21 -0
  102. package/skills/to-tickets/SKILL.md +106 -0
  103. package/skills/to-tickets/agents/openai.yaml +4 -0
  104. package/skills/wait-what/LICENSE +21 -0
  105. package/skills/wait-what/SKILL.md +8 -0
  106. package/skills/wait-what/agents/openai.yaml +4 -0
  107. package/skills/wayfinder/LICENSE +21 -0
  108. package/skills/wayfinder/SKILL.md +129 -0
  109. package/skills/wayfinder/agents/openai.yaml +4 -0
  110. package/skills/writing-for-agents/LICENSE +21 -0
  111. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  112. package/skills/writing-for-agents/SKILL.md +83 -0
  113. package/skills/writing-for-agents/agents/openai.yaml +3 -0
@@ -0,0 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { writeEnrolment } from './home.js';
3
+ const [house, environment] = process.argv.slice(2);
4
+ await writeEnrolment({ house: house, environment: environment, credential: readFileSync(0, 'utf8').trim() });
package/dist/files.js ADDED
@@ -0,0 +1,21 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export async function placeFiles(house, directory, files, signal) {
4
+ const paths = [];
5
+ for (const file of files) {
6
+ const original = await house.post('/kit/conversation/originals/get', { version: file.version, download: true }, signal);
7
+ const answer = await fetch(original.download.url, { signal });
8
+ if (!answer.ok)
9
+ throw new Error(`${file.name} did not download: ${answer.status}`);
10
+ const folder = join(directory, '.house', 'files', original.message, file.version);
11
+ await mkdir(join(directory, '.house')).catch((error) => {
12
+ if (error.code !== 'EEXIST')
13
+ throw error;
14
+ });
15
+ await mkdir(folder, { recursive: true });
16
+ const path = join(folder, file.name);
17
+ await writeFile(path, Buffer.from(await answer.arrayBuffer()));
18
+ paths.push(path);
19
+ }
20
+ return paths;
21
+ }
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { accessSync, constants, realpathSync, statSync } from 'node:fs';
4
+ import { request } from 'node:http';
5
+ import { constants as system } from 'node:os';
6
+ import { delimiter, dirname, join } from 'node:path';
7
+ import { WORKING_COPIES } from './git.js';
8
+ import { kitHome } from './home.js';
9
+ const VALUED = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--super-prefix']);
10
+ const READS = new Set([
11
+ 'blame',
12
+ 'branch',
13
+ 'cat-file',
14
+ 'check-ignore',
15
+ 'config',
16
+ 'describe',
17
+ 'diff',
18
+ 'fetch',
19
+ 'for-each-ref',
20
+ 'grep',
21
+ 'help',
22
+ 'log',
23
+ 'ls-files',
24
+ 'ls-tree',
25
+ 'reflog',
26
+ 'remote',
27
+ 'rev-list',
28
+ 'rev-parse',
29
+ 'shortlog',
30
+ 'show',
31
+ 'show-ref',
32
+ 'status',
33
+ 'tag',
34
+ 'version',
35
+ ]);
36
+ const RELAYED = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'];
37
+ function nativeGit() {
38
+ const shim = join(kitHome(), 'shim');
39
+ for (const directory of (process.env.PATH ?? '').split(delimiter)) {
40
+ if (directory === '')
41
+ continue;
42
+ try {
43
+ if (realpathSync(directory) === realpathSync(shim))
44
+ continue;
45
+ const candidate = join(directory, 'git');
46
+ if (!statSync(candidate).isFile())
47
+ continue;
48
+ accessSync(candidate, constants.X_OK);
49
+ return candidate;
50
+ }
51
+ catch { }
52
+ }
53
+ return null;
54
+ }
55
+ function captured(git, args) {
56
+ const ran = spawnSync(git, args, {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ maxBuffer: Infinity,
59
+ env: { ...process.env, LC_ALL: 'C' },
60
+ });
61
+ return ran.status === 0 ? ran.stdout.toString('utf8') : null;
62
+ }
63
+ function delegated(git, args) {
64
+ return new Promise((resolve) => {
65
+ const child = spawn(git, args, { stdio: 'inherit' });
66
+ const relay = (signal) => child.kill(signal);
67
+ for (const signal of RELAYED)
68
+ process.on(signal, relay);
69
+ const settle = (outcome) => {
70
+ for (const signal of RELAYED)
71
+ process.off(signal, relay);
72
+ resolve(outcome);
73
+ };
74
+ child.once('error', (error) => {
75
+ process.stderr.write(`git: ${error.message}\n`);
76
+ settle({ status: 127, signal: null });
77
+ });
78
+ child.once('close', (status, signal) => settle({ status, signal }));
79
+ });
80
+ }
81
+ function staged(listing) {
82
+ const entries = new Map();
83
+ for (const record of (listing ?? '').split('\0')) {
84
+ const tab = record.indexOf('\t');
85
+ if (tab >= 0)
86
+ entries.set(record.slice(tab + 1), record.slice(0, tab).split(' ')[1]);
87
+ }
88
+ return entries;
89
+ }
90
+ function moved(planned, before, after) {
91
+ const moves = [];
92
+ for (const line of planned.split('\n')) {
93
+ if (!line.startsWith('Renaming '))
94
+ continue;
95
+ const renaming = line.slice('Renaming '.length);
96
+ for (let at = renaming.indexOf(' to '); at >= 0; at = renaming.indexOf(' to ', at + 1)) {
97
+ const from = renaming.slice(0, at);
98
+ const to = renaming.slice(at + 4);
99
+ if (before.has(from) && !after.has(from) && after.get(to) === before.get(from)) {
100
+ moves.push({ from, to });
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ return moves;
106
+ }
107
+ function reported(socketPath, body) {
108
+ return new Promise((resolve) => {
109
+ const sent = request({ socketPath, path: '/git/observed', method: 'POST' }, (answer) => {
110
+ answer.resume();
111
+ answer.on('end', resolve);
112
+ });
113
+ sent.on('error', () => resolve());
114
+ sent.end(JSON.stringify(body));
115
+ });
116
+ }
117
+ async function observed(git, args) {
118
+ let index = 0;
119
+ while (index < args.length && args[index].startsWith('-'))
120
+ index += VALUED.has(args[index]) ? 2 : 1;
121
+ const globals = args.slice(0, index);
122
+ const name = args[index];
123
+ const bridge = process.env.HOUSE_BRIDGE;
124
+ if (bridge === undefined || name === undefined || READS.has(name))
125
+ return delegated(git, args);
126
+ const located = captured(git, [...globals, 'rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
127
+ const [common, worktree] = located?.split('\n') ?? [];
128
+ if (common === undefined || worktree === undefined || !dirname(common).startsWith(`${WORKING_COPIES}/`)) {
129
+ return delegated(git, args);
130
+ }
131
+ const listing = [...globals, '-C', worktree, 'ls-files', '--stage', '-z'];
132
+ const before = name === 'mv' ? staged(captured(git, listing)) : null;
133
+ const planned = name === 'mv' ? (captured(git, [...globals, 'mv', '--dry-run', ...args.slice(index + 1)]) ?? '') : '';
134
+ const outcome = await delegated(git, args);
135
+ const moves = before !== null && outcome.status === 0 ? moved(planned, before, staged(captured(git, listing))) : [];
136
+ await reported(bridge, { common, worktree, moves });
137
+ return outcome;
138
+ }
139
+ const git = nativeGit();
140
+ if (git === null) {
141
+ process.stderr.write('git: no installed Git found on PATH\n');
142
+ process.exitCode = 127;
143
+ }
144
+ else {
145
+ observed(git, process.argv.slice(2)).then(({ status, signal }) => {
146
+ if (signal === null) {
147
+ process.exitCode = status ?? 1;
148
+ return;
149
+ }
150
+ process.exitCode = 128 + system.signals[signal];
151
+ process.kill(process.pid, signal);
152
+ });
153
+ }
package/dist/git.js ADDED
@@ -0,0 +1,81 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { withoutProxy } from './clis.js';
4
+ export const WORKING_COPIES = '/agents/house/working-copies';
5
+ export const HOUSE_REF = 'refs/house/received';
6
+ const COMMITTER = 'House Kit <house-kit@localhost>';
7
+ export function run(cwd, args, input, env = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
10
+ const stdout = [];
11
+ let stderr = '';
12
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
13
+ child.stderr.on('data', (chunk) => {
14
+ stderr += chunk.toString('utf8');
15
+ });
16
+ child.on('error', reject);
17
+ child.on('close', (status) => resolve({ status, stdout: Buffer.concat(stdout), stderr }));
18
+ child.stdin.on('error', () => undefined);
19
+ child.stdin.end(input);
20
+ });
21
+ }
22
+ export async function git(cwd, args, input, env) {
23
+ const ran = await run(cwd, args, input, env);
24
+ if (ran.status !== 0)
25
+ throw new Error(`git ${args[0]} failed: ${ran.stderr.trim()}`);
26
+ return ran.stdout.toString('utf8');
27
+ }
28
+ export async function succeeded(cwd, args, env) {
29
+ return (await run(cwd, args, undefined, env)).status === 0;
30
+ }
31
+ export async function revision(cwd, name) {
32
+ return (await git(cwd, ['rev-parse', '--verify', `${name}^{commit}`])).trim();
33
+ }
34
+ export async function tree(repository, commit) {
35
+ const listed = await git(repository, ['ls-tree', '-r', '-z', commit]);
36
+ const entries = new Map();
37
+ for (const entry of listed.split('\0')) {
38
+ const tab = entry.indexOf('\t');
39
+ if (tab < 0)
40
+ continue;
41
+ const [, type, object] = entry.slice(0, tab).split(' ');
42
+ if (type === 'blob')
43
+ entries.set(entry.slice(tab + 1), object);
44
+ }
45
+ return entries;
46
+ }
47
+ export async function blobText(repository, object, path) {
48
+ const bytes = (await run(repository, ['cat-file', 'blob', object])).stdout;
49
+ try {
50
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
51
+ }
52
+ catch {
53
+ throw new Error(`${path} is not UTF-8 text`);
54
+ }
55
+ }
56
+ export function blobId(content) {
57
+ const bytes = Buffer.from(content, 'utf8');
58
+ return createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
59
+ }
60
+ function quoted(path) {
61
+ return `"${path.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n')}"`;
62
+ }
63
+ function data(text) {
64
+ const bytes = Buffer.from(text, 'utf8');
65
+ return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from('\n')];
66
+ }
67
+ export async function importCommits(repository, commits) {
68
+ const stream = [];
69
+ for (const [index, commit] of commits.entries()) {
70
+ stream.push(Buffer.from(`commit ${HOUSE_REF}\nmark :${index + 1}\ncommitter ${COMMITTER} ${Math.floor(Date.now() / 1000)} +0000\n`), ...data(commit.message), ...(commit.from === null ? [] : [Buffer.from(`from ${commit.from}\n`)]), ...(commit.merge === undefined ? [] : [Buffer.from(`merge ${commit.merge}\n`)]), ...(commit.replaces === true ? [Buffer.from('deleteall\n')] : []));
71
+ for (const [path, content] of commit.files) {
72
+ if (content === null)
73
+ stream.push(Buffer.from(`D ${quoted(path)}\n`));
74
+ else
75
+ stream.push(Buffer.from(`M 100644 inline ${quoted(path)}\n`), ...data(content));
76
+ }
77
+ stream.push(Buffer.from('\n'));
78
+ }
79
+ await git(repository, ['fast-import', '--quiet', '--force'], Buffer.concat(stream));
80
+ return revision(repository, HOUSE_REF);
81
+ }
package/dist/home.js CHANGED
@@ -2,12 +2,13 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  const ENROLMENT = 'credential.json';
5
+ const CONFIGURATION = 'kit.json';
5
6
  export function kitHome() {
6
7
  return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
7
8
  }
8
- export async function readEnrolment() {
9
+ export async function readHome(name) {
9
10
  try {
10
- return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
11
+ return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
11
12
  }
12
13
  catch (error) {
13
14
  if (error.code === 'ENOENT')
@@ -15,10 +16,22 @@ export async function readEnrolment() {
15
16
  throw error;
16
17
  }
17
18
  }
18
- export async function writeEnrolment(enrolment) {
19
+ export async function writeHome(name, value) {
19
20
  const home = kitHome();
20
21
  await mkdir(home, { recursive: true, mode: 0o700 });
21
- const temporary = join(home, `${ENROLMENT}.${process.pid}`);
22
- await writeFile(temporary, `${JSON.stringify(enrolment)}\n`, { mode: 0o600 });
23
- await rename(temporary, join(home, ENROLMENT));
22
+ const temporary = join(home, `${name}.${process.pid}`);
23
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
24
+ await rename(temporary, join(home, name));
25
+ }
26
+ export function readEnrolment() {
27
+ return readHome(ENROLMENT);
28
+ }
29
+ export function writeEnrolment(enrolment) {
30
+ return writeHome(ENROLMENT, enrolment);
31
+ }
32
+ export async function readConfiguration() {
33
+ return { skills: (await readHome(CONFIGURATION))?.skills !== false };
34
+ }
35
+ export function writeConfiguration(configuration) {
36
+ return writeHome(CONFIGURATION, configuration);
24
37
  }
@@ -1,4 +1,107 @@
1
1
  #!/usr/bin/env node
2
- process.stderr.write('house: house runs inside an Agent conversation\n');
3
- process.exitCode = 1;
4
- export {};
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { request } from 'node:http';
5
+ import { basename, join } from 'node:path';
6
+ import { kitHome } from './home.js';
7
+ const USAGE = "usage: house <tool> ['<arguments as JSON>']";
8
+ const GIT_HELP = 'Submit a Room commit to House; default HEAD. Use --owner only outside an Agent conversation. Native git push is not a House remote.';
9
+ const GIT_USAGE = `usage: house git push [commit] [--owner]\n${GIT_HELP}`;
10
+ const socketPath = process.env.HOUSE_BRIDGE;
11
+ function bridged(path, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
12
+ return new Promise((resolve, reject) => {
13
+ const sent = request({ socketPath: to, path, method: 'POST', headers }, (answer) => {
14
+ let text = '';
15
+ answer.setEncoding('utf8');
16
+ answer.on('data', (chunk) => {
17
+ text += chunk;
18
+ });
19
+ answer.on('end', () => {
20
+ if (answer.statusCode === 200)
21
+ resolve(text);
22
+ else
23
+ reject(new Error(`${path} answered ${answer.statusCode}: ${text}`));
24
+ });
25
+ });
26
+ sent.on('error', reject);
27
+ sent.end(body instanceof Buffer ? body : JSON.stringify(body));
28
+ });
29
+ }
30
+ async function mcp(method, params) {
31
+ const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: 1, method, params }));
32
+ if (answer.error !== undefined)
33
+ throw new Error(answer.error.message);
34
+ return answer.result;
35
+ }
36
+ async function tools() {
37
+ return (await mcp('tools/list', {})).tools;
38
+ }
39
+ async function upload(path, roomRef) {
40
+ const bytes = await readFile(path);
41
+ const declared = JSON.parse(await bridged('/kit/attachments/upload', {
42
+ ...(roomRef === undefined ? {} : { room_ref: roomRef }),
43
+ version: randomUUID(),
44
+ name: basename(path),
45
+ bytes: bytes.length,
46
+ sha256: createHash('sha256').update(bytes).digest('hex'),
47
+ }));
48
+ return bridged(declared.upload.url, bytes, {
49
+ 'content-type': 'application/octet-stream',
50
+ 'x-house-byte-operation': declared.upload.operation,
51
+ });
52
+ }
53
+ async function gitPush(args) {
54
+ if (args.includes('--help'))
55
+ return GIT_USAGE;
56
+ if (args[0] !== 'push')
57
+ throw new Error(GIT_USAGE);
58
+ const owner = args.includes('--owner');
59
+ const commit = args.slice(1).find((arg) => arg !== '--owner') ?? 'HEAD';
60
+ if (owner && socketPath !== undefined)
61
+ throw new Error('house git push --owner runs outside an Agent conversation');
62
+ if (!owner && socketPath === undefined)
63
+ throw new Error('outside an Agent conversation, house git push needs --owner');
64
+ const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
65
+ if (pushed.refused)
66
+ throw new Error(pushed.text);
67
+ return pushed.text;
68
+ }
69
+ async function house([verb, argument]) {
70
+ if (verb === undefined || verb === '--help') {
71
+ return [USAGE, `git push [commit]: ${GIT_HELP}`, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
72
+ }
73
+ if (argument === '--help') {
74
+ const tool = (await tools()).find((listed) => listed.name === verb);
75
+ if (tool === undefined)
76
+ throw new Error(`House has no tool ${verb}`);
77
+ return `${tool.description}\n${JSON.stringify(tool.inputSchema, null, 2)}`;
78
+ }
79
+ const args = (argument === undefined ? {} : JSON.parse(argument));
80
+ if (verb === 'upload_attachment')
81
+ return upload(String(args.path), args.room_ref);
82
+ if (verb === 'append_record' && Array.isArray(args.attachments)) {
83
+ const attachments = [];
84
+ for (const path of args.attachments) {
85
+ attachments.push(JSON.parse(await upload(String(path), args.room_ref)).attachment);
86
+ }
87
+ args.attachments = attachments;
88
+ }
89
+ const result = await mcp('tools/call', { name: verb, arguments: args });
90
+ const text = result.content.map((content) => content.text).join('\n');
91
+ if (result.isError === true)
92
+ throw new Error(text);
93
+ return text;
94
+ }
95
+ const argv = process.argv.slice(2);
96
+ if (argv[0] !== 'git' && socketPath === undefined) {
97
+ process.stderr.write('house: house runs inside an Agent conversation\n');
98
+ process.exitCode = 1;
99
+ }
100
+ else {
101
+ (argv[0] === 'git' ? gitPush(argv.slice(1)) : house(argv)).then((text) => {
102
+ process.stdout.write(`${text}\n`);
103
+ }, (error) => {
104
+ process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
105
+ process.exitCode = 1;
106
+ });
107
+ }
@@ -0,0 +1,33 @@
1
+ import { readEnrolment } from './home.js';
2
+ import { HOW_WE_WORK } from './skills.js';
3
+ 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 in your answer.';
5
+ const COPIES_LINE = 'These House paths are Git working copies here; commit in one and run `house git push` to send the commit to House:';
6
+ const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
7
+ const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
8
+ async function howWeWork(bridge) {
9
+ let document = '';
10
+ for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
11
+ const read = await bridge.tool('shell', { command });
12
+ const text = read.content.map((content) => content.text).join('\n');
13
+ const lines = text.split('\n');
14
+ let at = lines[5]?.startsWith('authority: ') ? 6 : 5;
15
+ if (read.isError === true || lines[2] !== 'exit: 0') {
16
+ const refusal = read.isError === true ? text : lines.slice(at).join('\n').replace(/^stderr: /gm, '');
17
+ if (UNREADABLE.test(refusal))
18
+ return '';
19
+ throw new Error(refusal);
20
+ }
21
+ const cut = lines[3] === 'truncation: egress' ? CUT.exec(lines[at++] ?? '') : null;
22
+ const shown = Buffer.from(`${lines.slice(at).join('\n')}\n`);
23
+ document += (cut === null ? shown : shown.subarray(0, Number(cut[1]))).toString();
24
+ command = cut?.[2];
25
+ }
26
+ return document.replace(/\n$/, '');
27
+ }
28
+ export async function instructions(bridge, base, copies) {
29
+ const app = new URL('/app/new.git', (await readEnrolment()).house).href;
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])];
32
+ return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
33
+ }
package/dist/kit-main.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { setGlobalProxyFromEnv } from 'node:http';
2
3
  import { login } from './login.js';
3
4
  import { resident } from './resident.js';
4
5
  const [command, ...argv] = process.argv.slice(2);
6
+ setGlobalProxyFromEnv();
5
7
  async function run() {
6
8
  if (command === 'login')
7
9
  return login(argv);
package/dist/login.js CHANGED
@@ -26,8 +26,9 @@ function listen() {
26
26
  server.close();
27
27
  server.closeAllConnections();
28
28
  };
29
+ const port = Number(process.env.HOUSE_KIT_LOGIN_PORT ?? 0);
29
30
  server.once('error', reject);
30
- server.listen(0, '127.0.0.1', () => {
31
+ server.listen(port, port === 0 ? '127.0.0.1' : '0.0.0.0', () => {
31
32
  const address = server.address();
32
33
  if (address === null || typeof address === 'string') {
33
34
  reject(new Error('the login callback has no port'));
@@ -49,8 +50,11 @@ function listen() {
49
50
  }
50
51
  async function typed(question) {
51
52
  const reader = createInterface({ input: process.stdin, output: process.stdout });
53
+ const closed = new Promise((_, reject) => {
54
+ reader.once('close', () => reject(new Error('no code was typed')));
55
+ });
52
56
  try {
53
- return (await reader.question(question)).trim();
57
+ return (await Promise.race([reader.question(question), closed])).trim();
54
58
  }
55
59
  finally {
56
60
  reader.close();
@@ -0,0 +1,9 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ export function operationId() {
3
+ const bytes = randomBytes(16);
4
+ bytes.writeUIntBE(Date.now(), 0, 6);
5
+ bytes[6] = 0x70 | (bytes[6] & 0x0f);
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)}`;
9
+ }
package/dist/resident.js CHANGED
@@ -1,10 +1,121 @@
1
- import { readEnrolment } from './home.js';
1
+ import { once } from 'node:events';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { extname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { killDescendants } from './acp.js';
7
+ import { Agents } from './agents.js';
8
+ import { house } from './api.js';
9
+ import { received } from './bridge.js';
10
+ import { Conversations } from './conversations.js';
11
+ import { WorkingCopies } from './copies.js';
12
+ import { kitHome, readEnrolment } from './home.js';
13
+ import { SignIns } from './sign-in.js';
14
+ import { placeSkillSet } from './skills.js';
2
15
  import { holdStream } from './stream.js';
16
+ import { Updates } from './update.js';
17
+ function report(error) {
18
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
19
+ }
20
+ function logged(work) {
21
+ work.catch(report);
22
+ }
23
+ function quoted(value) {
24
+ return `'${value.replaceAll("'", `'\\''`)}'`;
25
+ }
26
+ async function installShim() {
27
+ const main = fileURLToPath(new URL(`./git-main${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
28
+ await mkdir(join(kitHome(), 'shim'), { recursive: true });
29
+ await writeFile(join(kitHome(), 'shim', 'git'), `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, {
30
+ mode: 0o755,
31
+ });
32
+ }
33
+ async function serveOwner(copies) {
34
+ const socketPath = join(kitHome(), 'owner.sock');
35
+ await rm(socketPath, { force: true });
36
+ const server = createServer(async (request, response) => {
37
+ try {
38
+ if (request.url !== '/git/push') {
39
+ response.writeHead(404).end();
40
+ return;
41
+ }
42
+ const { cwd, commit } = JSON.parse(await received(request));
43
+ const pushed = await copies.push(cwd, commit, copies.owner);
44
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(pushed));
45
+ }
46
+ catch (error) {
47
+ response.writeHead(502).end(error instanceof Error ? error.message : String(error));
48
+ }
49
+ });
50
+ server.listen(socketPath);
51
+ await once(server, 'listening');
52
+ }
3
53
  export async function resident() {
4
54
  if ((await readEnrolment()) === null) {
5
55
  process.stderr.write('kit: this Environment is not enrolled; run kit login\n');
6
56
  process.exitCode = 1;
7
57
  return;
8
58
  }
9
- holdStream();
59
+ await placeSkillSet();
60
+ process.env.HOUSE_KIT_RESIDENT = String(process.pid);
61
+ const agents = new Agents(house);
62
+ const copies = new WorkingCopies(house);
63
+ await copies.load();
64
+ await installShim();
65
+ await serveOwner(copies);
66
+ let stream = null;
67
+ let reported = false;
68
+ const idle = () => conversations.idle() && signIns.idle();
69
+ const changed = () => {
70
+ if (!idle()) {
71
+ reported = false;
72
+ }
73
+ else if (!reported) {
74
+ reported = true;
75
+ logged(house.deliver('/kit/idle', {}, idle));
76
+ }
77
+ };
78
+ const conversations = new Conversations({
79
+ house,
80
+ agents,
81
+ copies,
82
+ send: (frame) => stream?.send(frame) ?? false,
83
+ changed,
84
+ });
85
+ const signIns = new SignIns(house, agents, changed);
86
+ const updates = new Updates(house);
87
+ for (const signal of ['SIGTERM', 'SIGINT']) {
88
+ process.once(signal, () => {
89
+ killDescendants();
90
+ process.kill(process.pid, signal);
91
+ });
92
+ }
93
+ await house.deliver('/kit/restarted', {}).catch(report);
94
+ await updates.resumed().catch(report);
95
+ logged(agents.refresh());
96
+ stream = holdStream({
97
+ opened: () => {
98
+ conversations.opened();
99
+ changed();
100
+ logged(copies.select());
101
+ },
102
+ frame: (arrived) => {
103
+ if (arrived.type === 'work_available' && arrived.subject === 'agents')
104
+ logged(agents.refresh());
105
+ if (arrived.type === 'work_available' && arrived.subject === 'working_copy')
106
+ logged(copies.select());
107
+ if (arrived.type === 'entries') {
108
+ copies.entries(arrived);
109
+ }
110
+ if (arrived.type === 'input') {
111
+ reported = false;
112
+ if (arrived.kind === 'sign_in')
113
+ signIns.start(arrived);
114
+ else if (arrived.kind === 'update')
115
+ updates.start(arrived);
116
+ else
117
+ conversations.input(arrived);
118
+ }
119
+ },
120
+ });
10
121
  }
@@ -0,0 +1,16 @@
1
+ export async function holdSecretInput(house, subject, steps, waiting) {
2
+ while (waiting()) {
3
+ try {
4
+ const held = await house.post(`/kit/secret-input/${subject}`, { steps });
5
+ if (held.outcome === 'collected')
6
+ return held.content;
7
+ if (held.release !== 'held')
8
+ return null;
9
+ }
10
+ catch (error) {
11
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
12
+ await new Promise((resolve) => setTimeout(resolve, 1000));
13
+ }
14
+ }
15
+ return null;
16
+ }