@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cc6246211e00c

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 (130) hide show
  1. package/README.md +42 -41
  2. package/dist/acp.js +38 -17
  3. package/dist/agents.js +111 -24
  4. package/dist/api.js +7 -3
  5. package/dist/app-git.js +10 -26
  6. package/dist/authority-main.js +2 -2
  7. package/dist/bridge.js +11 -25
  8. package/dist/building.js +46 -0
  9. package/dist/calls.js +63 -0
  10. package/dist/chatgpt.js +32 -0
  11. package/dist/clis-main.js +2 -2
  12. package/dist/clis.js +87 -22
  13. package/dist/connection.js +126 -0
  14. package/dist/conversations.js +410 -156
  15. package/dist/copies.js +185 -123
  16. package/dist/end.js +23 -4
  17. package/dist/enrol-main.js +7 -1
  18. package/dist/events.js +6 -2
  19. package/dist/files.js +139 -31
  20. package/dist/git.js +13 -11
  21. package/dist/home.js +96 -7
  22. package/dist/house-cli.js +299 -0
  23. package/dist/house-main.js +7 -177
  24. package/dist/instructions.js +8 -10
  25. package/dist/kit-cli.js +68 -0
  26. package/dist/kit-main.js +8 -22
  27. package/dist/login.js +111 -23
  28. package/dist/logout.js +0 -2
  29. package/dist/mcp.js +4 -2
  30. package/dist/parts.js +34 -16
  31. package/dist/placement.js +39 -0
  32. package/dist/relay.js +33 -2
  33. package/dist/resident.js +106 -44
  34. package/dist/scope.js +3 -0
  35. package/dist/service-main.js +9 -0
  36. package/dist/service.js +55 -0
  37. package/dist/sign-in.js +22 -39
  38. package/dist/skills.js +49 -35
  39. package/dist/transcription.js +85 -45
  40. package/dist/translate.js +2 -8
  41. package/dist/update.js +98 -53
  42. package/dist/web-wizard.js +101 -0
  43. package/dist/wire.js +22 -0
  44. package/dist/work.js +3 -5
  45. package/package.json +20 -20
  46. package/dist/bootstrap.json +0 -24
  47. package/dist/stream.js +0 -72
  48. package/how-we-work.md +0 -23
  49. package/skills/ask-matt/LICENSE +0 -21
  50. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  51. package/skills/ask-matt/SKILL.md +0 -91
  52. package/skills/ask-matt/agents/openai.yaml +0 -4
  53. package/skills/code-review/LICENSE +0 -21
  54. package/skills/code-review/SKILL.md +0 -89
  55. package/skills/code-review/agents/openai.yaml +0 -3
  56. package/skills/codebase-design/DEEPENING.md +0 -37
  57. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  58. package/skills/codebase-design/LICENSE +0 -21
  59. package/skills/codebase-design/SKILL.md +0 -116
  60. package/skills/codebase-design/agents/openai.yaml +0 -3
  61. package/skills/diagnosing-bugs/LICENSE +0 -21
  62. package/skills/diagnosing-bugs/SKILL.md +0 -140
  63. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  64. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  65. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  66. package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
  67. package/skills/domain-modeling/LICENSE +0 -21
  68. package/skills/domain-modeling/SKILL.md +0 -76
  69. package/skills/domain-modeling/agents/openai.yaml +0 -3
  70. package/skills/grill-me/LICENSE +0 -21
  71. package/skills/grill-me/SKILL.md +0 -8
  72. package/skills/grill-me/agents/openai.yaml +0 -4
  73. package/skills/grill-with-docs/LICENSE +0 -21
  74. package/skills/grill-with-docs/SKILL.md +0 -8
  75. package/skills/grill-with-docs/agents/openai.yaml +0 -4
  76. package/skills/grilling/LICENSE +0 -21
  77. package/skills/grilling/SKILL.md +0 -30
  78. package/skills/grilling/agents/openai.yaml +0 -3
  79. package/skills/handoff/LICENSE +0 -21
  80. package/skills/handoff/SKILL.md +0 -17
  81. package/skills/handoff/agents/openai.yaml +0 -4
  82. package/skills/implement/LICENSE +0 -21
  83. package/skills/implement/SKILL.md +0 -16
  84. package/skills/implement/agents/openai.yaml +0 -4
  85. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  86. package/skills/improve-codebase-architecture/LICENSE +0 -21
  87. package/skills/improve-codebase-architecture/SKILL.md +0 -72
  88. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
  89. package/skills/prototype/LICENSE +0 -21
  90. package/skills/prototype/LOGIC.md +0 -67
  91. package/skills/prototype/SKILL.md +0 -28
  92. package/skills/prototype/UI.md +0 -112
  93. package/skills/prototype/agents/openai.yaml +0 -3
  94. package/skills/research/LICENSE +0 -21
  95. package/skills/research/SKILL.md +0 -14
  96. package/skills/research/agents/openai.yaml +0 -3
  97. package/skills/resolving-merge-conflicts/LICENSE +0 -21
  98. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  99. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  100. package/skills/tdd/LICENSE +0 -21
  101. package/skills/tdd/SKILL.md +0 -40
  102. package/skills/tdd/agents/openai.yaml +0 -3
  103. package/skills/tdd/mocking.md +0 -59
  104. package/skills/tdd/tests.md +0 -77
  105. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  106. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  107. package/skills/teach/LICENSE +0 -21
  108. package/skills/teach/MISSION-FORMAT.md +0 -31
  109. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  110. package/skills/teach/SKILL.md +0 -141
  111. package/skills/teach/agents/openai.yaml +0 -4
  112. package/skills/to-questionnaire/LICENSE +0 -21
  113. package/skills/to-questionnaire/SKILL.md +0 -55
  114. package/skills/to-questionnaire/agents/openai.yaml +0 -4
  115. package/skills/to-spec/LICENSE +0 -21
  116. package/skills/to-spec/SKILL.md +0 -76
  117. package/skills/to-spec/agents/openai.yaml +0 -4
  118. package/skills/to-tickets/LICENSE +0 -21
  119. package/skills/to-tickets/SKILL.md +0 -106
  120. package/skills/to-tickets/agents/openai.yaml +0 -4
  121. package/skills/wait-what/LICENSE +0 -21
  122. package/skills/wait-what/SKILL.md +0 -8
  123. package/skills/wait-what/agents/openai.yaml +0 -4
  124. package/skills/wayfinder/LICENSE +0 -21
  125. package/skills/wayfinder/SKILL.md +0 -129
  126. package/skills/wayfinder/agents/openai.yaml +0 -4
  127. package/skills/writing-for-agents/LICENSE +0 -21
  128. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  129. package/skills/writing-for-agents/SKILL.md +0 -83
  130. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/relay.js CHANGED
@@ -1,7 +1,38 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ const PROXY = /^https?_proxy$/i;
3
+ const LOOPBACK = ['127.0.0.1', 'localhost', '::1'];
4
+ let selected;
5
+ let restoreProxy;
6
+ export function selectRelay(relay) {
7
+ selected = relay === undefined || relay === '' ? null : relay;
8
+ proxyUnlessRelayed();
9
+ }
10
+ export function relayOrigin() {
11
+ if (selected !== undefined)
12
+ return selected ?? undefined;
13
+ const ambient = process.env.HOUSE_KIT_RELAY;
14
+ return ambient === undefined || ambient === '' ? undefined : ambient;
15
+ }
16
+ function unlessRelayed() {
17
+ if (relayOrigin() === undefined)
18
+ return { ...process.env };
19
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
20
+ }
21
+ export function proxyUnlessRelayed() {
22
+ restoreProxy?.();
23
+ restoreProxy = setGlobalProxyFromEnv(unlessRelayed());
24
+ }
25
+ export function childEnvironment() {
26
+ const environment = unlessRelayed();
27
+ if (!Object.keys(environment).some((name) => PROXY.test(name)))
28
+ return environment;
29
+ const bypassed = [environment.NO_PROXY ?? environment.no_proxy, ...LOOPBACK].filter((entry) => entry !== undefined && entry !== '').join(',');
30
+ return { ...environment, NO_PROXY: bypassed, no_proxy: bypassed };
31
+ }
1
32
  export function relayed(target) {
2
33
  const url = new URL(target);
3
- const relay = process.env.HOUSE_KIT_RELAY;
4
- if (relay === undefined || relay === '')
34
+ const relay = relayOrigin();
35
+ if (relay === undefined)
5
36
  return url;
6
37
  const through = new URL(`${url.host}${url.pathname}${url.search}`, `${relay}/`);
7
38
  if (url.protocol === 'wss:' || url.protocol === 'ws:')
package/dist/resident.js CHANGED
@@ -1,21 +1,25 @@
1
1
  import { once } from 'node:events';
2
2
  import { watch } from 'node:fs';
3
3
  import { mkdir, rm, writeFile } from 'node:fs/promises';
4
- import { createServer } from 'node:http';
4
+ import { createServer, get } from 'node:http';
5
5
  import { dirname, extname, join } from 'node:path';
6
+ import { DatabaseSync } from 'node:sqlite';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { Agents } from './agents.js';
8
9
  import { house } from './api.js';
9
10
  import { received } from './bridge.js';
10
- import { Conversations } from './conversations.js';
11
+ import { CONVERSATION_METHODS, Conversations } from './conversations.js';
11
12
  import { LocalCopies } from './copies.js';
12
- import { agentBase, enrolled, kitHome } from './home.js';
13
+ import { restart } from './end.js';
14
+ import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, readEnrolment, restartAsked } from './home.js';
15
+ import { Calls } from './calls.js';
13
16
  import { killDescendants } from './scope.js';
14
17
  import { quoted } from './shell.js';
15
18
  import { SignIns } from './sign-in.js';
16
19
  import { placeSkillSet } from './skills.js';
17
- import { holdStream } from './stream.js';
18
- import { Updates } from './update.js';
20
+ import { holdConnection } from './connection.js';
21
+ import { KitUpdate } from './update.js';
22
+ import { WebWizards } from './web-wizard.js';
19
23
  import { codexLauncher } from './work.js';
20
24
  function report(error) {
21
25
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -28,26 +32,66 @@ async function installShim(path, module) {
28
32
  await mkdir(dirname(path), { recursive: true });
29
33
  await writeFile(path, `#!/bin/sh\nexec ${quoted(process.execPath)} ${quoted(main)} "$@"\n`, { mode: 0o755 });
30
34
  }
35
+ function claimed(lock) {
36
+ try {
37
+ lock.exec('BEGIN EXCLUSIVE');
38
+ return true;
39
+ }
40
+ catch (error) {
41
+ if (error.errcode === 5)
42
+ return false;
43
+ throw error;
44
+ }
45
+ }
31
46
  async function serveOwner(copies) {
32
- const socketPath = join(kitHome(), 'owner.sock');
33
- await rm(socketPath, { force: true });
47
+ const lock = new DatabaseSync(ownerLock());
48
+ if (!claimed(lock)) {
49
+ lock.close();
50
+ const running = await runningResident();
51
+ throw new Error(`${running === null ? 'another Kit process' : `Kit process ${running}`} already runs this Environment from Kit home ${kitHome()}; stop it first to run another Kit process here`);
52
+ }
34
53
  const server = createServer(async (request, response) => {
35
54
  try {
36
- if (request.url !== '/git/push') {
55
+ const text = await received(request);
56
+ let answered;
57
+ if (request.url === '/resident') {
58
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ pid: process.pid }));
59
+ return;
60
+ }
61
+ if (request.url === '/git/fetch') {
62
+ answered = await copies.fetch();
63
+ }
64
+ else if (request.url === '/git/push') {
65
+ const { cwd, commit } = JSON.parse(text);
66
+ answered = await copies.push(cwd, commit, copies.owner);
67
+ }
68
+ else {
37
69
  response.writeHead(404).end();
38
70
  return;
39
71
  }
40
- const { cwd, commit } = JSON.parse(await received(request));
41
- const pushed = await copies.push(cwd, commit, copies.owner);
42
- response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(pushed));
72
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(answered));
43
73
  }
44
74
  catch (error) {
45
75
  response.writeHead(502).end(error instanceof Error ? error.message : String(error));
46
76
  }
47
77
  });
48
- server.listen(socketPath);
78
+ server.on('close', () => lock.close());
79
+ await rm(ownerSocket(), { force: true });
80
+ server.listen(ownerSocket());
49
81
  await once(server, 'listening');
50
82
  }
83
+ function runningResident() {
84
+ return new Promise((resolve, reject) => {
85
+ get({ socketPath: ownerSocket(), path: '/resident' }, (answer) => {
86
+ received(answer).then((text) => resolve(JSON.parse(text).pid), reject);
87
+ }).on('error', (error) => {
88
+ if (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
89
+ resolve(null);
90
+ else
91
+ reject(error);
92
+ });
93
+ });
94
+ }
51
95
  function connected() {
52
96
  return new Promise((resolve) => {
53
97
  const watcher = watch(kitHome(), () => {
@@ -64,44 +108,58 @@ function connected() {
64
108
  resolve();
65
109
  });
66
110
  }
67
- export async function resident() {
111
+ async function started() {
68
112
  await connected();
113
+ await forgetRestart();
69
114
  watch(kitHome(), () => {
70
115
  if (enrolled())
71
116
  return;
72
117
  killDescendants();
73
118
  process.exit(1);
74
119
  });
75
- await placeSkillSet();
120
+ await placeSkillSet((await readEnrolment()).house);
76
121
  await mkdir(agentBase(), { recursive: true });
77
- process.env.HOUSE_KIT_RESIDENT = String(process.pid);
78
- const agents = new Agents(house);
79
- const copies = new LocalCopies(house);
80
- await copies.load();
81
122
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
82
123
  await installShim(codexLauncher(), 'codex-main');
124
+ }
125
+ export async function resident() {
126
+ await mkdir(kitHome(), { recursive: true, mode: 0o700 });
127
+ const copies = new LocalCopies(house);
128
+ await copies.load();
83
129
  await serveOwner(copies);
84
- let stream = null;
130
+ await started().catch((error) => {
131
+ report(error);
132
+ process.exit(1);
133
+ });
134
+ process.env.HOUSE_KIT_RESIDENT = String(process.pid);
135
+ const agents = new Agents(house, () => changed());
136
+ let connection = null;
85
137
  let reported = false;
86
- const idle = () => conversations.idle() && signIns.idle();
138
+ const idle = () => calls.idle() && conversations.idle() && agents.idle();
87
139
  const changed = () => {
88
140
  if (!idle()) {
89
141
  reported = false;
90
142
  }
143
+ else if (restartAsked() && conversations.quiet()) {
144
+ restart('restarting for an updated CLI');
145
+ }
91
146
  else if (!reported) {
92
147
  reported = true;
93
148
  logged(house.deliver('/kit/idle', {}, idle));
94
149
  }
95
150
  };
151
+ const calls = new Calls(house, changed);
96
152
  const conversations = new Conversations({
97
153
  house,
98
154
  agents,
99
155
  copies,
100
- send: (frame) => stream?.send(frame) ?? false,
156
+ calls,
157
+ send: (notification) => connection?.send(notification) ?? false,
101
158
  changed,
102
159
  });
103
- const signIns = new SignIns(house, agents, changed);
104
- const updates = new Updates(house);
160
+ const signIns = new SignIns(house, agents, calls);
161
+ const update = new KitUpdate(calls);
162
+ const wizards = new WebWizards(calls, (notification) => connection?.send(notification) ?? false);
105
163
  for (const signal of ['SIGTERM', 'SIGINT']) {
106
164
  process.once(signal, () => {
107
165
  killDescendants();
@@ -109,33 +167,37 @@ export async function resident() {
109
167
  });
110
168
  }
111
169
  await house.deliver('/kit/restarted', {}).catch(report);
112
- await updates.resumed().catch(report);
170
+ await update.resumed().catch(report);
113
171
  logged(agents.refresh());
114
- stream = holdStream({
172
+ connection = holdConnection({
115
173
  opened: () => {
116
174
  conversations.opened();
117
175
  changed();
118
- logged(copies.select());
176
+ logged(copies.connected());
119
177
  },
120
- frame: (arrived) => {
121
- if (arrived.type === 'watching')
122
- conversations.watching(arrived.watching === true);
123
- if (arrived.type === 'work_available' && arrived.subject === 'agents')
124
- logged(agents.refresh());
125
- if (arrived.type === 'work_available' && arrived.subject === 'local_copy')
126
- logged(copies.select());
127
- if (arrived.type === 'entries') {
128
- copies.entries(arrived);
129
- }
130
- if (arrived.type === 'input') {
131
- reported = false;
132
- if (arrived.kind === 'sign_in')
133
- signIns.start(arrived);
134
- else if (arrived.kind === 'update')
135
- updates.start(arrived);
136
- else
137
- conversations.input(arrived);
178
+ closed: () => wizards.disconnected(),
179
+ call: (call) => {
180
+ if (call.kind === 'sign_in')
181
+ signIns.start(call);
182
+ else if (call.kind === 'update')
183
+ update.start(call);
184
+ else if (call.kind === 'run_wizard')
185
+ wizards.start(call);
186
+ else if (CONVERSATION_METHODS.has(call.kind))
187
+ conversations.call(call);
188
+ else {
189
+ const refused = { refused: `This Kit does not know ${call.kind}; update the Kit` };
190
+ void calls.carry(call.input_id, async () => ({ result: refused }));
138
191
  }
139
192
  },
193
+ notification: (notification) => {
194
+ if (notification.type === 'watching')
195
+ conversations.watching(notification.watching === true);
196
+ if (notification.type === 'authority_changed')
197
+ logged(copies.changed(notification.rooms));
198
+ if (notification.type === 'work_available' && notification.subject === 'agents')
199
+ logged(agents.refresh());
200
+ wizards.notified(notification);
201
+ },
140
202
  });
141
203
  }
package/dist/scope.js CHANGED
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  const LISTING_BYTES = 1 << 28;
5
5
  const CGROUPS = '/sys/fs/cgroup';
6
6
  const GONE = new Set(['ENOENT', 'ESRCH']);
7
+ const EXITED = new Set(['Z', 'X']);
7
8
  function failure(error) {
8
9
  return error.code ?? '';
9
10
  }
@@ -24,6 +25,8 @@ function read(pid) {
24
25
  try {
25
26
  const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
26
27
  const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
28
+ if (EXITED.has(fields[0]))
29
+ return null;
27
30
  const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
28
31
  return { parent: Number(fields[1]), session: fields[3], command };
29
32
  }
@@ -0,0 +1,9 @@
1
+ import { startService } from './service.js';
2
+ try {
3
+ if (startService())
4
+ process.stdout.write('started\n');
5
+ }
6
+ catch (error) {
7
+ process.stderr.write(`kit: ${error.message}\n`);
8
+ process.exitCode = 1;
9
+ }
@@ -0,0 +1,55 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { kitHome } from './home.js';
5
+ const UNIT = 'house-kit.service';
6
+ // launchctl and systemctl answer at once, so ten seconds only bounds a stuck service manager.
7
+ const MANAGER_MS = 10_000;
8
+ function manage(command, args) {
9
+ return spawnSync(command, args, { encoding: 'utf8', timeout: MANAGER_MS });
10
+ }
11
+ function start(command, args) {
12
+ const started = manage(command, args);
13
+ if (started.status === 0)
14
+ return;
15
+ const cause = started.error?.message ?? (started.stderr.trim() || `${command} exited with ${started.status ?? 'a signal'}`);
16
+ throw new Error(`the Kit service could not be started: ${cause}`);
17
+ }
18
+ function physical(path) {
19
+ return existsSync(path) ? realpathSync(path) : resolve(path);
20
+ }
21
+ function systemdHome(environment) {
22
+ for (const [, quoted, bare] of environment.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)) {
23
+ const assignment = bare ?? quoted.replace(/\\(.)/g, '$1');
24
+ if (assignment.startsWith('HOUSE_KIT_HOME='))
25
+ return assignment.slice('HOUSE_KIT_HOME='.length);
26
+ }
27
+ return undefined;
28
+ }
29
+ function runsThisHome(served) {
30
+ return served !== undefined && physical(served) === physical(kitHome());
31
+ }
32
+ export function startService() {
33
+ if (process.platform === 'darwin') {
34
+ const service = `gui/${process.getuid()}/house-kit`;
35
+ const printed = manage('launchctl', ['print', service]);
36
+ if (printed.status !== 0 || /^\tstate = running$/m.test(printed.stdout))
37
+ return false;
38
+ if (!runsThisHome(/^\t+HOUSE_KIT_HOME => (.+)$/m.exec(printed.stdout)?.[1]))
39
+ return false;
40
+ start('launchctl', ['kickstart', service]);
41
+ return true;
42
+ }
43
+ for (const scope of [['--user'], []]) {
44
+ if (manage('systemctl', [...scope, 'is-enabled', '--quiet', UNIT]).status !== 0)
45
+ continue;
46
+ const environment = manage('systemctl', [...scope, 'show', '--property=Environment', '--value', UNIT]).stdout;
47
+ if (!runsThisHome(systemdHome(environment)))
48
+ continue;
49
+ if (manage('systemctl', [...scope, 'is-active', '--quiet', UNIT]).status === 0)
50
+ return false;
51
+ start('systemctl', [...scope, 'start', UNIT]);
52
+ return true;
53
+ }
54
+ return false;
55
+ }
package/dist/sign-in.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { CLIS, withoutProxy } from './clis.js';
3
+ import { CLIS } from './clis.js';
4
+ import { childEnvironment } from './relay.js';
4
5
  import { killTree } from './scope.js';
5
6
  import { holdSecretInput } from './secret-input.js';
6
7
  const ESCAPE = /\u001b\[[\d;]*m/g;
@@ -9,72 +10,54 @@ const CODE = /^[A-Z0-9]+-[A-Z0-9]+$/;
9
10
  function causeOf(error) {
10
11
  return error instanceof Error ? error.message : String(error);
11
12
  }
12
- function logged(error) {
13
- process.stderr.write(`kit: ${causeOf(error)}\n`);
14
- }
15
13
  function endOf(signal) {
16
14
  return new Promise((resolve) => signal.addEventListener('abort', () => resolve(true), { once: true }));
17
15
  }
18
16
  export class SignIns {
19
17
  house;
20
18
  agents;
21
- changed;
22
- running = new Set();
23
- constructor(house, agents, changed) {
19
+ calls;
20
+ constructor(house, agents, calls) {
24
21
  this.house = house;
25
22
  this.agents = agents;
26
- this.changed = changed;
27
- }
28
- idle() {
29
- return this.running.size === 0;
23
+ this.calls = calls;
30
24
  }
31
- start(input) {
32
- if (this.running.has(input.input_id))
33
- return;
34
- this.running.add(input.input_id);
35
- this.changed();
36
- this.signIn(input)
37
- .catch(logged)
38
- .finally(() => {
39
- this.running.delete(input.input_id);
40
- this.changed();
41
- });
25
+ start(call) {
26
+ void this.calls.carry(call.input_id, () => this.signIn(call));
42
27
  }
43
- async signIn(input) {
28
+ async signIn(call) {
44
29
  const ending = new AbortController();
45
30
  // House stops waiting for a sign-in ten minutes after it wrote it and sends the seconds left, so its install and login end with the page.
46
- const timer = setTimeout(() => ending.abort(), input.ends_in * 1000);
47
- const ended = await Promise.race([this.signedIn(input, ending.signal), endOf(ending.signal)]);
31
+ const timer = setTimeout(() => ending.abort(), call.ends_in * 1000);
32
+ const ended = await Promise.race([this.signedIn(call, ending.signal), endOf(ending.signal)]);
48
33
  clearTimeout(timer);
49
34
  if (ended)
50
- process.stderr.write(`kit: the ${input.cli} sign-in reached the end House set for it\n`);
51
- await this.house.deliver(`/kit/inputs/${input.input_id}/ack`, {});
52
- if (!ended)
53
- await this.agents.reread(input.cli);
35
+ process.stderr.write(`kit: the ${call.cli} sign-in reached the end House set for it\n`);
36
+ return ended ? { result: {} } : { result: {}, returned: () => this.agents.reread(call.cli) };
54
37
  }
55
- async signedIn(input, signal) {
56
- await this.login(input, signal);
38
+ async signedIn(call, signal) {
39
+ await this.login(call, signal);
57
40
  if (!signal.aborted)
58
- await this.agents.recheck(input.cli, signal);
41
+ await this.agents.recheck(call.cli, signal);
59
42
  return false;
60
43
  }
61
- async login(input, signal) {
44
+ async login(call, signal) {
62
45
  let cli;
63
46
  try {
64
- cli = await this.agents.located(input.cli, signal);
47
+ cli = await this.agents.located(call.cli, signal);
65
48
  }
66
49
  catch (error) {
67
50
  if (!signal.aborted)
68
- process.stderr.write(`kit: the ${input.cli} sign-in failed: ${causeOf(error)}\n`);
51
+ process.stderr.write(`kit: the ${call.cli} sign-in failed: ${causeOf(error)}\n`);
69
52
  return;
70
53
  }
71
54
  if (signal.aborted)
72
55
  return;
73
- const login = CLIS[input.cli].login;
56
+ const login = CLIS[call.cli].login;
74
57
  const child = spawn(cli, login.args, {
75
58
  stdio: ['pipe', 'pipe', 'pipe'],
76
59
  detached: true,
77
- env: withoutProxy(),
60
+ env: childEnvironment(),
78
61
  });
79
62
  const running = () => child.exitCode === null && child.signalCode === null;
80
63
  const stop = () => killTree(child);
@@ -102,7 +85,7 @@ export class SignIns {
102
85
  return;
103
86
  held = true;
104
87
  const steps = [{ kind: 'visit', label: 'Open this link and sign in', url: link }, step];
105
- void holdSecretInput(this.house, input.input_id, steps, running).then((content) => {
88
+ void holdSecretInput(this.house, call.input_id, steps, running).then((content) => {
106
89
  if (!running())
107
90
  return;
108
91
  if (content === null)
@@ -119,6 +102,6 @@ export class SignIns {
119
102
  });
120
103
  signal.removeEventListener('abort', stop);
121
104
  if (failed !== null && !signal.aborted)
122
- process.stderr.write(`kit: the ${input.cli} sign-in failed: ${failed}\n`);
105
+ process.stderr.write(`kit: the ${call.cli} sign-in failed: ${failed}\n`);
123
106
  }
124
107
  }
package/dist/skills.js CHANGED
@@ -1,17 +1,14 @@
1
- import { readFileSync } from 'node:fs';
2
- import { cp, lstat, mkdir, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
1
+ import { cp, lstat, mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promises';
3
2
  import { homedir } from 'node:os';
4
3
  import { join, relative } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { KIT_VERSION } from './clis.js';
4
+ import { GUIDE, listSkills } from '@agentshouse/skills';
5
+ import { parse } from 'yaml';
6
+ import { KIT_RELEASE } from './clis.js';
7
7
  import { kitHome, readConfiguration, readHome, writeHome } from './home.js';
8
- import { refusalOf } from './refusals.js';
9
- export const HOW_WE_WORK = '/rooms/private/how-we-work.md';
10
- const SKILL_SET = fileURLToPath(new URL('../skills/', import.meta.url));
11
- const HOW_WE_WORK_TEXT = readFileSync(new URL('../how-we-work.md', import.meta.url), 'utf8');
12
8
  const RECORD = 'skill-set.json';
13
- const CREATED = 'how-we-work-created';
14
- const UNWRITABLE = new Set(['room_not_found', 'operation_denied']);
9
+ const ENTRY = 'SKILL.md';
10
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/u;
11
+ const ORIGIN = '<origin>';
15
12
  function skillsDirectory() {
16
13
  return join(homedir(), '.agents', 'skills');
17
14
  }
@@ -29,50 +26,67 @@ async function present(path) {
29
26
  throw error;
30
27
  }
31
28
  }
32
- async function install() {
33
- const names = [];
34
- for (const name of (await readdir(SKILL_SET)).sort()) {
35
- if (!(await present(join(skillsDirectory(), name))) && !(await present(join(claudeDirectory(), name)))) {
36
- names.push(name);
29
+ function skillName(text) {
30
+ const front = FRONTMATTER.exec(text);
31
+ const fields = (front === null ? null : parse(front[1]));
32
+ return typeof fields?.name === 'string' && typeof fields.description === 'string' ? fields.name : undefined;
33
+ }
34
+ function skillSet(origin) {
35
+ const upstream = listSkills().map(({ name, directory }) => ({
36
+ name,
37
+ place: (destination) => cp(directory, destination, { recursive: true }),
38
+ }));
39
+ const pages = GUIDE.flatMap(({ text }) => {
40
+ const name = skillName(text);
41
+ if (name === undefined)
42
+ return [];
43
+ const place = async (destination) => {
44
+ await mkdir(destination);
45
+ await writeFile(join(destination, ENTRY), text.replaceAll(ORIGIN, origin));
46
+ };
47
+ return [{ name, place }];
48
+ });
49
+ return [...upstream, ...pages].sort((left, right) => (left.name < right.name ? -1 : 1));
50
+ }
51
+ async function install(origin) {
52
+ const placed = [];
53
+ for (const skill of skillSet(origin)) {
54
+ if (!(await present(join(skillsDirectory(), skill.name))) && !(await present(join(claudeDirectory(), skill.name)))) {
55
+ placed.push(skill);
37
56
  }
38
57
  }
58
+ const names = placed.map((skill) => skill.name);
39
59
  await writeHome(RECORD, { names });
40
60
  await mkdir(skillsDirectory(), { recursive: true });
41
61
  await mkdir(claudeDirectory(), { recursive: true });
42
62
  const skills = await realpath(skillsDirectory());
43
63
  const claude = await realpath(claudeDirectory());
44
- for (const name of names) {
45
- await cp(join(SKILL_SET, name), join(skillsDirectory(), name), { recursive: true });
64
+ for (const { name, place } of placed) {
65
+ await place(join(skillsDirectory(), name));
46
66
  if (claude !== skills)
47
67
  await symlink(relative(claude, join(skills, name)), join(claudeDirectory(), name));
48
68
  }
49
- await writeHome(RECORD, { version: KIT_VERSION, names });
69
+ await writeHome(RECORD, { version: KIT_RELEASE, origin, names });
70
+ }
71
+ export async function placeSkillSet(origin) {
72
+ try {
73
+ await replaceSkillSet(origin);
74
+ }
75
+ catch (error) {
76
+ process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
77
+ }
50
78
  }
51
- export async function placeSkillSet() {
79
+ async function replaceSkillSet(origin) {
52
80
  const accepted = (await readConfiguration()).skills;
53
81
  const installed = await readHome(RECORD);
54
- if (accepted && installed?.version === KIT_VERSION)
82
+ if (accepted && installed?.version === KIT_RELEASE && installed.origin === origin)
55
83
  return;
56
84
  for (const name of installed?.names ?? []) {
57
85
  await rm(join(claudeDirectory(), name), { recursive: true, force: true });
58
86
  await rm(join(skillsDirectory(), name), { recursive: true, force: true });
59
87
  }
60
88
  if (accepted)
61
- await install();
89
+ await install(origin);
62
90
  else
63
91
  await rm(join(kitHome(), RECORD), { force: true });
64
92
  }
65
- export async function createHowWeWork(bridge) {
66
- if ((await readHome(RECORD)) === null || (await present(join(kitHome(), CREATED))))
67
- return;
68
- const created = await bridge.tool('edit', { changes: [{ op: 'create', path: HOW_WE_WORK, content: HOW_WE_WORK_TEXT }] });
69
- if (created.isError === true) {
70
- const text = created.content.map((content) => content.text).join('\n');
71
- const refusal = refusalOf(text);
72
- if (UNWRITABLE.has(refusal?.code ?? ''))
73
- return;
74
- if (refusal?.code !== 'edit_conflict' || !refusal.conflicts.includes('path_exists'))
75
- throw new Error(text);
76
- }
77
- await writeFile(join(kitHome(), CREATED), '');
78
- }