@agentshouse/kit 0.2.1-alpha.2 → 0.2.1-alpha.21

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 (114) hide show
  1. package/README.md +39 -3
  2. package/dist/abort.js +9 -0
  3. package/dist/acp.js +27 -7
  4. package/dist/agents.js +204 -52
  5. package/dist/api.js +6 -1
  6. package/dist/authority-main.js +22 -0
  7. package/dist/bootstrap.json +31 -0
  8. package/dist/bridge.js +178 -54
  9. package/dist/clis-main.js +23 -0
  10. package/dist/clis.js +106 -32
  11. package/dist/configure-main.js +6 -0
  12. package/dist/conversations.js +128 -36
  13. package/dist/copies.js +809 -0
  14. package/dist/enrol-main.js +4 -0
  15. package/dist/git-main.js +153 -0
  16. package/dist/git.js +85 -0
  17. package/dist/home.js +36 -7
  18. package/dist/house-main.js +112 -41
  19. package/dist/instructions.js +16 -10
  20. package/dist/kit-main.js +2 -0
  21. package/dist/login.js +29 -5
  22. package/dist/mcp.js +38 -0
  23. package/dist/operation.js +12 -0
  24. package/dist/refusals.js +41 -0
  25. package/dist/resident.js +63 -7
  26. package/dist/sign-in.js +21 -5
  27. package/dist/skills.js +78 -0
  28. package/dist/transcription.js +122 -0
  29. package/dist/translate.js +51 -0
  30. package/dist/update.js +117 -0
  31. package/how-we-work.md +23 -0
  32. package/package.json +13 -9
  33. package/skills/ask-matt/LICENSE +21 -0
  34. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  35. package/skills/ask-matt/SKILL.md +91 -0
  36. package/skills/ask-matt/agents/openai.yaml +4 -0
  37. package/skills/code-review/LICENSE +21 -0
  38. package/skills/code-review/SKILL.md +89 -0
  39. package/skills/code-review/agents/openai.yaml +3 -0
  40. package/skills/codebase-design/DEEPENING.md +37 -0
  41. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  42. package/skills/codebase-design/LICENSE +21 -0
  43. package/skills/codebase-design/SKILL.md +116 -0
  44. package/skills/codebase-design/agents/openai.yaml +3 -0
  45. package/skills/diagnosing-bugs/LICENSE +21 -0
  46. package/skills/diagnosing-bugs/SKILL.md +140 -0
  47. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  48. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  49. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  50. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  51. package/skills/domain-modeling/LICENSE +21 -0
  52. package/skills/domain-modeling/SKILL.md +76 -0
  53. package/skills/domain-modeling/agents/openai.yaml +3 -0
  54. package/skills/grill-me/LICENSE +21 -0
  55. package/skills/grill-me/SKILL.md +8 -0
  56. package/skills/grill-me/agents/openai.yaml +4 -0
  57. package/skills/grill-with-docs/LICENSE +21 -0
  58. package/skills/grill-with-docs/SKILL.md +8 -0
  59. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  60. package/skills/grilling/LICENSE +21 -0
  61. package/skills/grilling/SKILL.md +30 -0
  62. package/skills/grilling/agents/openai.yaml +3 -0
  63. package/skills/handoff/LICENSE +21 -0
  64. package/skills/handoff/SKILL.md +17 -0
  65. package/skills/handoff/agents/openai.yaml +4 -0
  66. package/skills/implement/LICENSE +21 -0
  67. package/skills/implement/SKILL.md +16 -0
  68. package/skills/implement/agents/openai.yaml +4 -0
  69. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  70. package/skills/improve-codebase-architecture/LICENSE +21 -0
  71. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  72. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  73. package/skills/prototype/LICENSE +21 -0
  74. package/skills/prototype/LOGIC.md +67 -0
  75. package/skills/prototype/SKILL.md +28 -0
  76. package/skills/prototype/UI.md +112 -0
  77. package/skills/prototype/agents/openai.yaml +3 -0
  78. package/skills/research/LICENSE +21 -0
  79. package/skills/research/SKILL.md +14 -0
  80. package/skills/research/agents/openai.yaml +3 -0
  81. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  82. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  83. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  84. package/skills/tdd/LICENSE +21 -0
  85. package/skills/tdd/SKILL.md +40 -0
  86. package/skills/tdd/agents/openai.yaml +3 -0
  87. package/skills/tdd/mocking.md +59 -0
  88. package/skills/tdd/tests.md +77 -0
  89. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  90. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  91. package/skills/teach/LICENSE +21 -0
  92. package/skills/teach/MISSION-FORMAT.md +31 -0
  93. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  94. package/skills/teach/SKILL.md +141 -0
  95. package/skills/teach/agents/openai.yaml +4 -0
  96. package/skills/to-questionnaire/LICENSE +21 -0
  97. package/skills/to-questionnaire/SKILL.md +55 -0
  98. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  99. package/skills/to-spec/LICENSE +21 -0
  100. package/skills/to-spec/SKILL.md +76 -0
  101. package/skills/to-spec/agents/openai.yaml +4 -0
  102. package/skills/to-tickets/LICENSE +21 -0
  103. package/skills/to-tickets/SKILL.md +106 -0
  104. package/skills/to-tickets/agents/openai.yaml +4 -0
  105. package/skills/wait-what/LICENSE +21 -0
  106. package/skills/wait-what/SKILL.md +8 -0
  107. package/skills/wait-what/agents/openai.yaml +4 -0
  108. package/skills/wayfinder/LICENSE +21 -0
  109. package/skills/wayfinder/SKILL.md +129 -0
  110. package/skills/wayfinder/agents/openai.yaml +4 -0
  111. package/skills/writing-for-agents/LICENSE +21 -0
  112. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  113. package/skills/writing-for-agents/SKILL.md +83 -0
  114. 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() });
@@ -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 { workingCopies } 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(`${workingCopies()}/`)) {
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,85 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { withoutProxy } from './clis.js';
5
+ import { workspaceRoot } from './home.js';
6
+ export function workingCopies() {
7
+ return join(workspaceRoot(), 'working-copies');
8
+ }
9
+ export const HOUSE_REF = 'refs/house/received';
10
+ const COMMITTER = 'House Kit <house-kit@localhost>';
11
+ export function run(cwd, args, input, env = {}) {
12
+ return new Promise((resolve, reject) => {
13
+ const child = spawn('git', args, { cwd, env: { ...withoutProxy(), ...env } });
14
+ const stdout = [];
15
+ let stderr = '';
16
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
17
+ child.stderr.on('data', (chunk) => {
18
+ stderr += chunk.toString('utf8');
19
+ });
20
+ child.on('error', reject);
21
+ child.on('close', (status) => resolve({ status, stdout: Buffer.concat(stdout), stderr }));
22
+ child.stdin.on('error', () => undefined);
23
+ child.stdin.end(input);
24
+ });
25
+ }
26
+ export async function git(cwd, args, input, env) {
27
+ const ran = await run(cwd, args, input, env);
28
+ if (ran.status !== 0)
29
+ throw new Error(`git ${args[0]} failed: ${ran.stderr.trim()}`);
30
+ return ran.stdout.toString('utf8');
31
+ }
32
+ export async function succeeded(cwd, args, env) {
33
+ return (await run(cwd, args, undefined, env)).status === 0;
34
+ }
35
+ export async function revision(cwd, name) {
36
+ return (await git(cwd, ['rev-parse', '--verify', `${name}^{commit}`])).trim();
37
+ }
38
+ export async function tree(repository, commit) {
39
+ const listed = await git(repository, ['ls-tree', '-r', '-z', commit]);
40
+ const entries = new Map();
41
+ for (const entry of listed.split('\0')) {
42
+ const tab = entry.indexOf('\t');
43
+ if (tab < 0)
44
+ continue;
45
+ const [, type, object] = entry.slice(0, tab).split(' ');
46
+ if (type === 'blob')
47
+ entries.set(entry.slice(tab + 1), object);
48
+ }
49
+ return entries;
50
+ }
51
+ export async function blobText(repository, object, path) {
52
+ const bytes = (await run(repository, ['cat-file', 'blob', object])).stdout;
53
+ try {
54
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
55
+ }
56
+ catch {
57
+ throw new Error(`${path} is binary and House stores only text; remove it from the commit`);
58
+ }
59
+ }
60
+ export function blobId(content) {
61
+ const bytes = Buffer.from(content, 'utf8');
62
+ return createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
63
+ }
64
+ function quoted(path) {
65
+ return `"${path.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n')}"`;
66
+ }
67
+ function data(text) {
68
+ const bytes = Buffer.from(text, 'utf8');
69
+ return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from('\n')];
70
+ }
71
+ export async function importCommits(repository, commits) {
72
+ const stream = [];
73
+ for (const [index, commit] of commits.entries()) {
74
+ 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')] : []));
75
+ for (const [path, content] of commit.files) {
76
+ if (content === null)
77
+ stream.push(Buffer.from(`D ${quoted(path)}\n`));
78
+ else
79
+ stream.push(Buffer.from(`M 100644 inline ${quoted(path)}\n`), ...data(content));
80
+ }
81
+ stream.push(Buffer.from('\n'));
82
+ }
83
+ await git(repository, ['fast-import', '--quiet', '--force'], Buffer.concat(stream));
84
+ return revision(repository, HOUSE_REF);
85
+ }
package/dist/home.js CHANGED
@@ -1,13 +1,21 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, rename, rm, 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';
6
+ const OWN_AGENT = 'own-agent.json';
5
7
  export function kitHome() {
6
8
  return process.env.HOUSE_KIT_HOME ?? join(homedir(), '.house-kit');
7
9
  }
8
- export async function readEnrolment() {
10
+ export function workspaceRoot() {
11
+ return process.env.HOUSE_KIT_WORKSPACE ?? '/agents/house';
12
+ }
13
+ export function agentBase() {
14
+ return join(workspaceRoot(), 'agents');
15
+ }
16
+ export async function readHome(name) {
9
17
  try {
10
- return JSON.parse(await readFile(join(kitHome(), ENROLMENT), 'utf8'));
18
+ return JSON.parse(await readFile(join(kitHome(), name), 'utf8'));
11
19
  }
12
20
  catch (error) {
13
21
  if (error.code === 'ENOENT')
@@ -15,10 +23,31 @@ export async function readEnrolment() {
15
23
  throw error;
16
24
  }
17
25
  }
18
- export async function writeEnrolment(enrolment) {
26
+ export async function writeHome(name, value) {
19
27
  const home = kitHome();
20
28
  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));
29
+ const temporary = join(home, `${name}.${process.pid}`);
30
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
31
+ await rename(temporary, join(home, name));
32
+ }
33
+ export function readEnrolment() {
34
+ return readHome(ENROLMENT);
35
+ }
36
+ export function writeEnrolment(enrolment) {
37
+ return writeHome(ENROLMENT, enrolment);
38
+ }
39
+ export function readOwnAgent() {
40
+ return readHome(OWN_AGENT);
41
+ }
42
+ export function writeOwnAgent(ownAgent) {
43
+ return writeHome(OWN_AGENT, ownAgent);
44
+ }
45
+ export function forgetOwnAgent() {
46
+ return rm(join(kitHome(), OWN_AGENT), { force: true });
47
+ }
48
+ export async function readConfiguration() {
49
+ return { skills: (await readHome(CONFIGURATION))?.skills !== false };
50
+ }
51
+ export function writeConfiguration(configuration) {
52
+ return writeHome(CONFIGURATION, configuration);
24
53
  }
@@ -1,13 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { createHash, randomUUID } from 'node:crypto';
2
+ import { createHash } from 'node:crypto';
3
3
  import { readFile } from 'node:fs/promises';
4
- import { request } from 'node:http';
5
- import { basename } from 'node:path';
4
+ import { request, setGlobalProxyFromEnv } from 'node:http';
5
+ import { basename, join } from 'node:path';
6
+ import { kitHome, readOwnAgent } from './home.js';
7
+ import { callHouse, rpc, UNREACHABLE } from './mcp.js';
8
+ import { refusalOf } from './refusals.js';
6
9
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
10
+ const GIT_USAGE = 'usage: house git push [commit] [--owner]';
11
+ const ARGUMENTS = `the arguments are one JSON object in single quotes, like house search '{"query":"invoice"}'`;
12
+ const CLOSED = "this conversation's House connection is closed; nothing to do from here";
13
+ const REVOKED = 'this conversation no longer has House access; nothing to do from here';
14
+ const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
15
+ const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
7
16
  const socketPath = process.env.HOUSE_BRIDGE;
8
- function bridged(path, body) {
17
+ function refused(path, status, text) {
18
+ const refusal = refusalOf(text);
19
+ if (refusal !== null)
20
+ return refusal.text;
21
+ if (status === 401)
22
+ return REVOKED;
23
+ if (URL.canParse(path))
24
+ return `the upload answered ${status}; upload the file again`;
25
+ if (status === 502 && /^[A-Za-z]/.test(text))
26
+ return text;
27
+ return `${path} answered ${status}`;
28
+ }
29
+ function bridged(path, body, headers = { 'content-type': 'application/json' }, to = socketPath) {
9
30
  return new Promise((resolve, reject) => {
10
- const sent = request({ socketPath, path, method: 'POST', headers: { 'content-type': 'application/json' } }, (answer) => {
31
+ const sent = request({ socketPath: to, path, method: 'POST', headers }, (answer) => {
11
32
  let text = '';
12
33
  answer.setEncoding('utf8');
13
34
  answer.on('data', (chunk) => {
@@ -17,15 +38,28 @@ function bridged(path, body) {
17
38
  if (answer.statusCode === 200)
18
39
  resolve(text);
19
40
  else
20
- reject(new Error(`${path} answered ${answer.statusCode}: ${text}`));
41
+ reject(new Error(refused(path, answer.statusCode, text)));
21
42
  });
22
43
  });
23
- sent.on('error', reject);
24
- sent.end(JSON.stringify(body));
44
+ sent.on('error', () => reject(new Error(CLOSED)));
45
+ sent.end(body instanceof Buffer ? body : JSON.stringify(body));
46
+ });
47
+ }
48
+ async function direct(message) {
49
+ const ownAgent = await readOwnAgent();
50
+ if (ownAgent === null)
51
+ throw new Error(UNCONNECTED);
52
+ setGlobalProxyFromEnv();
53
+ const answer = await callHouse(ownAgent.house, ownAgent.credential, message).catch(() => {
54
+ throw new Error(UNREACHABLE);
25
55
  });
56
+ if (answer.status === 200)
57
+ return answer.text;
58
+ throw new Error(answer.status === 401 ? DISCONNECTED : (refusalOf(answer.text)?.text ?? `House answered ${answer.status}`));
26
59
  }
27
60
  async function mcp(method, params) {
28
- const answer = JSON.parse(await bridged('/', { jsonrpc: '2.0', id: 1, method, params }));
61
+ const message = rpc(method, params);
62
+ const answer = JSON.parse(socketPath === undefined ? await direct(message) : await bridged('/', message));
29
63
  if (answer.error !== undefined)
30
64
  throw new Error(answer.error.message);
31
65
  return answer.result;
@@ -33,60 +67,97 @@ async function mcp(method, params) {
33
67
  async function tools() {
34
68
  return (await mcp('tools/list', {})).tools;
35
69
  }
36
- async function upload(path, roomRef) {
37
- const bytes = await readFile(path);
70
+ async function localFile(path) {
71
+ try {
72
+ return await readFile(path);
73
+ }
74
+ catch (error) {
75
+ throw new Error(error.code === 'EISDIR'
76
+ ? `${path} is a folder; upload one file at a time`
77
+ : `${path} is not a readable file`);
78
+ }
79
+ }
80
+ async function upload(path, roomRef, missing) {
81
+ if (typeof path !== 'string' || path === '')
82
+ throw new Error(missing);
83
+ const bytes = await localFile(path);
38
84
  const declared = JSON.parse(await bridged('/kit/attachments/upload', {
39
85
  ...(roomRef === undefined ? {} : { room_ref: roomRef }),
40
- version: randomUUID(),
41
86
  name: basename(path),
42
87
  bytes: bytes.length,
43
88
  sha256: createHash('sha256').update(bytes).digest('hex'),
44
89
  }));
45
- const answer = await fetch(declared.upload.url, {
46
- method: 'POST',
47
- headers: { 'content-type': 'application/octet-stream', 'x-house-byte-operation': declared.upload.operation },
48
- body: bytes,
49
- });
50
- const text = await answer.text();
51
- if (!answer.ok)
52
- throw new Error(`House refused the bytes of ${path} with ${answer.status}: ${text}`);
53
- return text;
90
+ if (declared.upload === undefined)
91
+ return declared.attachment;
92
+ const saved = JSON.parse(await bridged(declared.upload.url, bytes, {
93
+ 'content-type': 'application/octet-stream',
94
+ 'x-house-byte-operation': declared.upload.operation,
95
+ }));
96
+ if (saved.upload_failure !== undefined)
97
+ throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
98
+ return saved.attachment;
99
+ }
100
+ async function gitPush(args) {
101
+ if (args.includes('--help'))
102
+ return GIT_USAGE;
103
+ if (args[0] !== 'push')
104
+ throw new Error('only house git push [commit] exists; use plain git for the rest');
105
+ const owner = args.includes('--owner');
106
+ const commit = args.slice(1).find((arg) => arg !== '--owner') ?? 'HEAD';
107
+ if (owner && socketPath !== undefined)
108
+ throw new Error('--owner is not for Agent conversations; run house git push without it');
109
+ if (!owner && socketPath === undefined)
110
+ throw new Error('outside an Agent conversation, house git push needs --owner');
111
+ const pushed = JSON.parse(await bridged('/git/push', { cwd: process.cwd(), commit }, undefined, owner ? join(kitHome(), 'owner.sock') : socketPath));
112
+ if (pushed.refused)
113
+ throw new Error(pushed.text);
114
+ return pushed.text;
115
+ }
116
+ function argumentsOf(argument) {
117
+ if (argument === undefined)
118
+ return {};
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(argument);
122
+ }
123
+ catch {
124
+ throw new Error(ARGUMENTS);
125
+ }
126
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
127
+ throw new Error(ARGUMENTS);
128
+ return parsed;
54
129
  }
55
130
  async function house([verb, argument]) {
56
131
  if (verb === undefined || verb === '--help') {
57
- return [USAGE, ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
132
+ return [USAGE, 'git push [commit]', ...(await tools()).map((tool) => `${tool.name}: ${tool.description}`)].join('\n');
58
133
  }
59
134
  if (argument === '--help') {
60
135
  const tool = (await tools()).find((listed) => listed.name === verb);
61
136
  if (tool === undefined)
62
- throw new Error(`House has no tool ${verb}`);
63
- return `${tool.description}\n${JSON.stringify(tool.inputSchema, null, 2)}`;
137
+ throw new Error(`${verb} is no Tool; house find_command '{"query":"${verb}"}' finds Commands`);
138
+ return `${tool.description}\n${JSON.stringify(tool.inputSchema)}`;
64
139
  }
65
- const args = (argument === undefined ? {} : JSON.parse(argument));
66
- if (verb === 'upload_attachment')
67
- return upload(String(args.path), args.room_ref);
68
- if (verb === 'append_record' && Array.isArray(args.attachments)) {
140
+ const args = argumentsOf(argument);
141
+ if (socketPath !== undefined && verb === 'upload_attachment') {
142
+ return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload_attachment needs "path", a local file') });
143
+ }
144
+ if (socketPath !== undefined && verb === 'append_record' && Array.isArray(args.attachments)) {
69
145
  const attachments = [];
70
146
  for (const path of args.attachments) {
71
- attachments.push(JSON.parse(await upload(String(path), args.room_ref)).attachment);
147
+ attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
72
148
  }
73
149
  args.attachments = attachments;
74
150
  }
75
151
  const result = await mcp('tools/call', { name: verb, arguments: args });
76
152
  const text = result.content.map((content) => content.text).join('\n');
77
153
  if (result.isError === true)
78
- throw new Error(text);
154
+ throw new Error(text.replace(/\n+$/, ''));
79
155
  return text;
80
156
  }
81
- if (socketPath === undefined) {
82
- process.stderr.write('house: house runs inside an Agent conversation\n');
157
+ const argv = process.argv.slice(2);
158
+ (argv[0] === 'git' ? gitPush(argv.slice(1)) : house(argv)).then((text) => {
159
+ process.stdout.write(`${text}\n`);
160
+ }, (error) => {
161
+ process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
83
162
  process.exitCode = 1;
84
- }
85
- else {
86
- house(process.argv.slice(2)).then((text) => {
87
- process.stdout.write(`${text}\n`);
88
- }, (error) => {
89
- process.stderr.write(`house: ${error instanceof Error ? error.message : String(error)}\n`);
90
- process.exitCode = 1;
91
- });
92
- }
163
+ });
@@ -1,28 +1,34 @@
1
+ import { HOW_WE_WORK } from './skills.js';
1
2
  const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
2
- const FILE_LINE = 'To give the User a file, upload it with `house upload_attachment` and link it in your answer.';
3
- const HOW_WE_WORK = '/private/library/how-we-work.md';
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 READ_LINE = 'Before working in a Room, read `/private/rooms/<handle>/AGENTS.md`, and before editing its Wiki, `/rooms/<handle>/library/wiki/RULES.md`.';
5
+ const COPIES_LINE = 'House working copies; commit, then `house git push`:';
4
6
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
7
+ const FAILED = /^exit: [1-9]\d*$/;
5
8
  const CUT = /^stderr: shell: output_cut (\d+) of \d+ bytes; continue with: (.+)$/;
9
+ function cutOf(lines) {
10
+ const cut = CUT.exec(lines[0] ?? '');
11
+ return cut !== null && Buffer.byteLength(lines.slice(1).join('\n')) === Number(cut[1]) ? cut : null;
12
+ }
6
13
  async function howWeWork(bridge) {
7
14
  let document = '';
8
15
  for (let command = `cat ${HOW_WE_WORK}`; command !== undefined;) {
9
16
  const read = await bridge.tool('shell', { command });
10
17
  const text = read.content.map((content) => content.text).join('\n');
11
18
  const lines = text.split('\n');
12
- let at = lines[5]?.startsWith('authority: ') ? 6 : 5;
13
- if (read.isError === true || lines[2] !== 'exit: 0') {
14
- const refusal = read.isError === true ? text : lines.slice(at).join('\n').replace(/^stderr: /gm, '');
19
+ if (read.isError === true || (FAILED.test(lines[0] ?? '') && lines[1]?.startsWith('stderr: '))) {
20
+ const refusal = read.isError === true ? text : lines.slice(1).join('\n').replace(/^stderr: /gm, '');
15
21
  if (UNREADABLE.test(refusal))
16
22
  return '';
17
23
  throw new Error(refusal);
18
24
  }
19
- const cut = lines[3] === 'truncation: egress' ? CUT.exec(lines[at++] ?? '') : null;
20
- const shown = Buffer.from(`${lines.slice(at).join('\n')}\n`);
21
- document += (cut === null ? shown : shown.subarray(0, Number(cut[1]))).toString();
25
+ const cut = cutOf(lines);
26
+ document += lines.slice(cut === null ? 0 : 1).join('\n');
22
27
  command = cut?.[2];
23
28
  }
24
29
  return document.replace(/\n$/, '');
25
30
  }
26
- export async function instructions(bridge, base) {
27
- return [`${HOUSE_LINE}\n${FILE_LINE}`, base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
31
+ export async function instructions(bridge, base, copies) {
32
+ const lines = [HOUSE_LINE, FILE_LINE, READ_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
33
+ return [lines.join('\n'), base, await howWeWork(bridge)].filter((part) => part !== '').join('\n\n');
28
34
  }
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
@@ -4,7 +4,8 @@ import { hostname } from 'node:os';
4
4
  import { createInterface } from 'node:readline/promises';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { post } from './api.js';
7
- import { readEnrolment, writeEnrolment } from './home.js';
7
+ import { forgetOwnAgent, readEnrolment, readOwnAgent, writeEnrolment, writeOwnAgent } from './home.js';
8
+ import { callHouse, rpc } from './mcp.js';
8
9
  const DEFAULT_HOUSE = 'https://agents.house';
9
10
  function listen() {
10
11
  return new Promise((resolve, reject) => {
@@ -26,8 +27,9 @@ function listen() {
26
27
  server.close();
27
28
  server.closeAllConnections();
28
29
  };
30
+ const port = Number(process.env.HOUSE_KIT_LOGIN_PORT ?? 0);
29
31
  server.once('error', reject);
30
- server.listen(0, '127.0.0.1', () => {
32
+ server.listen(port, port === 0 ? '127.0.0.1' : '0.0.0.0', () => {
31
33
  const address = server.address();
32
34
  if (address === null || typeof address === 'string') {
33
35
  reject(new Error('the login callback has no port'));
@@ -47,10 +49,21 @@ function listen() {
47
49
  });
48
50
  });
49
51
  }
52
+ async function accepted(house, credential) {
53
+ const checked = await callHouse(house, credential, rpc('tools/list', {}));
54
+ if (checked.status === 200)
55
+ return true;
56
+ if (checked.status === 401)
57
+ return false;
58
+ throw new Error(`House answered ${checked.status} when Kit checked this computer's own agent's connection: ${checked.text}`);
59
+ }
50
60
  async function typed(question) {
51
61
  const reader = createInterface({ input: process.stdin, output: process.stdout });
62
+ const closed = new Promise((_, reject) => {
63
+ reader.once('close', () => reject(new Error('no code was typed')));
64
+ });
52
65
  try {
53
- return (await reader.question(question)).trim();
66
+ return (await Promise.race([reader.question(question), closed])).trim();
54
67
  }
55
68
  finally {
56
69
  reader.close();
@@ -67,18 +80,29 @@ export async function login(argv) {
67
80
  });
68
81
  const enrolled = await readEnrolment();
69
82
  const house = values.house ?? enrolled?.house ?? DEFAULT_HOUSE;
70
- const environment = values.environment ?? (enrolled?.house === house ? enrolled.environment : undefined);
83
+ const replaces = process.env.HOUSE_KIT_REPLACES;
84
+ const environment = replaces === undefined ? (values.environment ?? (enrolled?.house === house ? enrolled.environment : undefined)) : undefined;
85
+ const ownAgent = await readOwnAgent();
86
+ const held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
71
87
  const verifier = randomBytes(32).toString('base64url');
72
88
  const started = {
73
89
  ...(environment === undefined
74
- ? { act: 'new_environment', label: hostname() }
90
+ ? { act: 'new_environment', label: hostname(), ...(replaces === undefined ? {} : { replaces }) }
75
91
  : { act: 'existing_environment', environment_id: environment }),
76
92
  code_challenge: createHash('sha256').update(verifier).digest('base64url'),
93
+ ...(held ? {} : { headless: true }),
77
94
  };
78
95
  const exchange = async (code) => {
79
96
  const exchanged = await post(house, '/kit/token', { code, code_verifier: verifier });
80
97
  await writeEnrolment({ house, environment: exchanged.environment, credential: exchanged.credential });
81
98
  process.stdout.write(`Environment ${exchanged.environment} is connected to ${house}.\n`);
99
+ if (exchanged.headless_credential !== undefined) {
100
+ await writeOwnAgent({ house, user: exchanged.user, credential: exchanged.headless_credential });
101
+ }
102
+ else if (held && ownAgent.user !== exchanged.user) {
103
+ await forgetOwnAgent();
104
+ process.stdout.write("This computer's own agent was another User's; run kit login again to connect it to you.\n");
105
+ }
82
106
  };
83
107
  if (values.manual) {
84
108
  const { start_url } = await post(house, '/kit', {