@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c83d468784cc9

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 (144) hide show
  1. package/README.md +28 -28
  2. package/dist/acp.js +90 -19
  3. package/dist/afk.js +17 -0
  4. package/dist/agents.js +87 -44
  5. package/dist/api.js +5 -5
  6. package/dist/authority-main.js +3 -3
  7. package/dist/bridge.js +5 -5
  8. package/dist/building.js +50 -0
  9. package/dist/calls.js +71 -0
  10. package/dist/chunks.js +49 -0
  11. package/dist/clis.js +45 -40
  12. package/dist/{stream.js → connection.js} +34 -52
  13. package/dist/conversations.js +498 -178
  14. package/dist/copies.js +315 -162
  15. package/dist/end.js +14 -4
  16. package/dist/events.js +66 -8
  17. package/dist/files.js +139 -37
  18. package/dist/git.js +47 -13
  19. package/dist/home.js +0 -7
  20. package/dist/house-cli.js +95 -87
  21. package/dist/house-main.js +3 -3
  22. package/dist/instructions.js +29 -9
  23. package/dist/kit-cli.js +4 -4
  24. package/dist/kit-main.js +3 -3
  25. package/dist/login.js +4 -4
  26. package/dist/logout.js +1 -1
  27. package/dist/mcp.js +6 -2
  28. package/dist/parts.js +10 -0
  29. package/dist/placement.js +21 -20
  30. package/dist/relay.js +21 -0
  31. package/dist/resident.js +51 -38
  32. package/dist/scope.js +15 -4
  33. package/dist/scripts.js +255 -0
  34. package/dist/secret-input.js +1 -1
  35. package/dist/service.js +23 -1
  36. package/dist/sign-in.js +22 -39
  37. package/dist/silence.js +28 -0
  38. package/dist/skills.js +42 -97
  39. package/dist/transcription.js +83 -43
  40. package/dist/translate.js +2 -8
  41. package/dist/update.js +113 -127
  42. package/dist/wire.js +37 -3
  43. package/package.json +20 -21
  44. package/dist/bootstrap.json +0 -24
  45. package/dist/clis-main.js +0 -23
  46. package/how-we-work.md +0 -33
  47. package/skills/ask-matt/LICENSE +0 -21
  48. package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
  49. package/skills/ask-matt/SKILL.md +0 -97
  50. package/skills/ask-matt/agents/openai.yaml +0 -5
  51. package/skills/code-review/LICENSE +0 -21
  52. package/skills/code-review/SKILL.md +0 -89
  53. package/skills/code-review/agents/openai.yaml +0 -3
  54. package/skills/codebase-design/DEEPENING.md +0 -37
  55. package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
  56. package/skills/codebase-design/LICENSE +0 -21
  57. package/skills/codebase-design/SKILL.md +0 -114
  58. package/skills/codebase-design/agents/openai.yaml +0 -3
  59. package/skills/diagnosing-bugs/LICENSE +0 -21
  60. package/skills/diagnosing-bugs/SKILL.md +0 -138
  61. package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
  62. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
  63. package/skills/domain-modeling/ADR-FORMAT.md +0 -47
  64. package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
  65. package/skills/domain-modeling/LICENSE +0 -21
  66. package/skills/domain-modeling/SKILL.md +0 -74
  67. package/skills/domain-modeling/agents/openai.yaml +0 -3
  68. package/skills/grill-me/LICENSE +0 -21
  69. package/skills/grill-me/SKILL.md +0 -7
  70. package/skills/grill-me/agents/openai.yaml +0 -5
  71. package/skills/grill-with-docs/LICENSE +0 -21
  72. package/skills/grill-with-docs/SKILL.md +0 -7
  73. package/skills/grill-with-docs/agents/openai.yaml +0 -5
  74. package/skills/grilling/LICENSE +0 -21
  75. package/skills/grilling/SKILL.md +0 -30
  76. package/skills/grilling/agents/openai.yaml +0 -3
  77. package/skills/handoff/LICENSE +0 -21
  78. package/skills/handoff/SKILL.md +0 -16
  79. package/skills/handoff/agents/openai.yaml +0 -5
  80. package/skills/implement/LICENSE +0 -21
  81. package/skills/implement/SKILL.md +0 -17
  82. package/skills/implement/agents/openai.yaml +0 -5
  83. package/skills/implement-spec/LICENSE +0 -21
  84. package/skills/implement-spec/SKILL.md +0 -40
  85. package/skills/implement-spec/agents/openai.yaml +0 -5
  86. package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
  87. package/skills/improve-codebase-architecture/LICENSE +0 -21
  88. package/skills/improve-codebase-architecture/SKILL.md +0 -71
  89. package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  90. package/skills/pr/CREDITS.md +0 -3
  91. package/skills/pr/LICENSE +0 -21
  92. package/skills/pr/SKILL.md +0 -170
  93. package/skills/pr/agents/openai.yaml +0 -3
  94. package/skills/prototype/LICENSE +0 -21
  95. package/skills/prototype/LOGIC.md +0 -67
  96. package/skills/prototype/SKILL.md +0 -26
  97. package/skills/prototype/UI.md +0 -112
  98. package/skills/prototype/agents/openai.yaml +0 -3
  99. package/skills/research/LICENSE +0 -21
  100. package/skills/research/SKILL.md +0 -12
  101. package/skills/research/agents/openai.yaml +0 -3
  102. package/skills/retro/LICENSE +0 -21
  103. package/skills/retro/SKILL.md +0 -44
  104. package/skills/retro/agents/openai.yaml +0 -5
  105. package/skills/tdd/LICENSE +0 -21
  106. package/skills/tdd/SKILL.md +0 -38
  107. package/skills/tdd/agents/openai.yaml +0 -3
  108. package/skills/tdd/mocking.md +0 -59
  109. package/skills/tdd/tests.md +0 -77
  110. package/skills/teach/GLOSSARY-FORMAT.md +0 -35
  111. package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
  112. package/skills/teach/LICENSE +0 -21
  113. package/skills/teach/MISSION-FORMAT.md +0 -31
  114. package/skills/teach/RESOURCES-FORMAT.md +0 -32
  115. package/skills/teach/SKILL.md +0 -140
  116. package/skills/teach/agents/openai.yaml +0 -5
  117. package/skills/to-questionnaire/LICENSE +0 -21
  118. package/skills/to-questionnaire/SKILL.md +0 -54
  119. package/skills/to-questionnaire/agents/openai.yaml +0 -5
  120. package/skills/to-spec/LICENSE +0 -21
  121. package/skills/to-spec/SKILL.md +0 -75
  122. package/skills/to-spec/agents/openai.yaml +0 -5
  123. package/skills/to-tickets/LICENSE +0 -21
  124. package/skills/to-tickets/SKILL.md +0 -105
  125. package/skills/to-tickets/agents/openai.yaml +0 -5
  126. package/skills/triage/AGENT-BRIEF.md +0 -207
  127. package/skills/triage/LICENSE +0 -21
  128. package/skills/triage/OUT-OF-SCOPE.md +0 -105
  129. package/skills/triage/SKILL.md +0 -112
  130. package/skills/triage/agents/openai.yaml +0 -5
  131. package/skills/wait-what/LICENSE +0 -21
  132. package/skills/wait-what/SKILL.md +0 -7
  133. package/skills/wait-what/agents/openai.yaml +0 -5
  134. package/skills/wayfinder/LICENSE +0 -21
  135. package/skills/wayfinder/SKILL.md +0 -128
  136. package/skills/wayfinder/agents/openai.yaml +0 -5
  137. package/skills/wizard/LICENSE +0 -21
  138. package/skills/wizard/SKILL.md +0 -44
  139. package/skills/wizard/agents/openai.yaml +0 -3
  140. package/skills/wizard/template.sh +0 -223
  141. package/skills/writing-for-agents/LICENSE +0 -21
  142. package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
  143. package/skills/writing-for-agents/SKILL.md +0 -81
  144. package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/resident.js CHANGED
@@ -8,16 +8,19 @@ import { fileURLToPath } from 'node:url';
8
8
  import { Agents } from './agents.js';
9
9
  import { house } from './api.js';
10
10
  import { received } from './bridge.js';
11
- import { CONVERSATION_INPUTS, Conversations } from './conversations.js';
11
+ import { CONVERSATION_METHODS, Conversations } from './conversations.js';
12
12
  import { LocalCopies } from './copies.js';
13
- import { restart } from './end.js';
14
- import { agentBase, enrolled, forgetRestart, kitHome, ownerLock, ownerSocket, restartAsked } from './home.js';
13
+ import { restart, unknownMethod } from './end.js';
14
+ import { agentBase, enrolled, kitHome, ownerLock, ownerSocket, readEnrolment } from './home.js';
15
+ import { Calls } from './calls.js';
15
16
  import { killDescendants } from './scope.js';
17
+ import { removeInputs, Scripts } from './scripts.js';
16
18
  import { quoted } from './shell.js';
17
19
  import { SignIns } from './sign-in.js';
18
20
  import { placeSkillSet } from './skills.js';
19
- import { holdStream } from './stream.js';
20
- import { Updates } from './update.js';
21
+ import { holdConnection } from './connection.js';
22
+ import { KitUpdate } from './update.js';
23
+ import { takeUnservedRelease } from './wire.js';
21
24
  import { codexLauncher } from './work.js';
22
25
  function report(error) {
23
26
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -46,7 +49,7 @@ async function serveOwner(copies) {
46
49
  if (!claimed(lock)) {
47
50
  lock.close();
48
51
  const running = await runningResident();
49
- throw new Error(`${running === null ? 'another Kit' : `Kit ${running}`} already runs this Environment from Kit home ${kitHome()}; stop it first to run another here`);
52
+ 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`);
50
53
  }
51
54
  const server = createServer(async (request, response) => {
52
55
  try {
@@ -108,14 +111,13 @@ function connected() {
108
111
  }
109
112
  async function started() {
110
113
  await connected();
111
- await forgetRestart();
112
114
  watch(kitHome(), () => {
113
115
  if (enrolled())
114
116
  return;
115
117
  killDescendants();
116
118
  process.exit(1);
117
119
  });
118
- await placeSkillSet();
120
+ await placeSkillSet((await readEnrolment()).house);
119
121
  await mkdir(agentBase(), { recursive: true });
120
122
  await installShim(join(kitHome(), 'shim', 'git'), 'git-main');
121
123
  await installShim(codexLauncher(), 'codex-main');
@@ -125,71 +127,82 @@ export async function resident() {
125
127
  const copies = new LocalCopies(house);
126
128
  await copies.load();
127
129
  await serveOwner(copies);
130
+ await copies.swept();
128
131
  await started().catch((error) => {
129
132
  report(error);
130
133
  process.exit(1);
131
134
  });
132
135
  process.env.HOUSE_KIT_RESIDENT = String(process.pid);
133
136
  const agents = new Agents(house, () => changed());
134
- let stream = null;
137
+ let connection = null;
135
138
  let reported = false;
136
- const idle = () => conversations.idle() && signIns.idle() && agents.idle();
139
+ const idle = () => calls.idle() && conversations.idle() && agents.idle();
137
140
  const changed = () => {
138
141
  if (!idle()) {
139
142
  reported = false;
140
143
  }
141
- else if (restartAsked() && conversations.quiet()) {
142
- restart('restarting for an updated CLI');
144
+ else if (update.placedRelease() !== null && conversations.quiet()) {
145
+ restart(`Kit ${update.placedRelease()} is placed; its supervisor starts it`);
143
146
  }
144
147
  else if (!reported) {
145
148
  reported = true;
146
- logged(house.deliver('/kit/idle', {}, idle));
149
+ logged(house.deliver('/kit/kits/report-idle', {}, idle));
147
150
  }
148
151
  };
152
+ const calls = new Calls(house, changed);
149
153
  const conversations = new Conversations({
150
154
  house,
151
155
  agents,
152
156
  copies,
153
- send: (frame) => stream?.send(frame) ?? false,
157
+ calls,
158
+ send: (oneWayCall) => connection?.send(oneWayCall) ?? false,
154
159
  changed,
155
160
  });
156
- const signIns = new SignIns(house, agents, changed);
157
- const updates = new Updates(house);
161
+ const signIns = new SignIns(house, agents, calls);
162
+ const update = new KitUpdate(calls);
163
+ if (process.env.HOUSE_KIT_SANDBOX !== '1')
164
+ takeUnservedRelease((release) => logged(update.takeRefused(release)));
165
+ removeInputs();
166
+ const scripts = new Scripts(calls, (oneWayCall) => connection?.send(oneWayCall) ?? false);
158
167
  for (const signal of ['SIGTERM', 'SIGINT']) {
159
168
  process.once(signal, () => {
160
169
  killDescendants();
170
+ removeInputs();
161
171
  process.kill(process.pid, signal);
162
172
  });
163
173
  }
164
- await house.deliver('/kit/restarted', {}).catch(report);
165
- await updates.resumed().catch(report);
174
+ await house.deliver('/kit/kits/report-restart', {}).catch(report);
166
175
  logged(agents.refresh());
167
- stream = holdStream({
176
+ agents.keepCurrent();
177
+ connection = holdConnection({
168
178
  opened: () => {
169
179
  conversations.opened();
170
180
  changed();
171
181
  logged(copies.connected());
172
182
  },
173
- frame: (arrived) => {
174
- if (arrived.type === 'watching')
175
- conversations.watching(arrived.watching === true);
176
- if (arrived.type === 'authority_changed')
177
- logged(copies.connected());
178
- if (arrived.type === 'work_available' && arrived.subject === 'agents')
179
- logged(agents.refresh());
180
- if (arrived.type === 'input') {
181
- reported = false;
182
- if (arrived.kind === 'sign_in')
183
- signIns.start(arrived);
184
- else if (arrived.kind === 'update')
185
- updates.start(arrived);
186
- else if (CONVERSATION_INPUTS.has(arrived.kind))
187
- conversations.input(arrived);
188
- else {
189
- const refused = { refused: `This Kit does not know ${arrived.kind}; update the Kit` };
190
- logged(house.deliver(`/kit/inputs/${arrived.input_id}/ack`, refused).finally(changed));
191
- }
183
+ closed: () => scripts.disconnected(),
184
+ call: (call) => {
185
+ if (call.kind === 'sign_in')
186
+ signIns.start(call);
187
+ else if (call.kind === 'update')
188
+ update.take(call);
189
+ else if (call.kind === 'run_wizard' || call.kind === 'run_function')
190
+ scripts.start(call);
191
+ else if (CONVERSATION_METHODS.has(call.kind))
192
+ conversations.call(call);
193
+ else {
194
+ const refused = { refused: unknownMethod(call.kind) };
195
+ void calls.carry(call.input_id, async () => ({ result: refused }));
192
196
  }
193
197
  },
198
+ oneWayCall: (oneWayCall) => {
199
+ if (oneWayCall.type === 'watching')
200
+ conversations.watching(oneWayCall.watching === true);
201
+ if (oneWayCall.type === 'authority_changed')
202
+ logged(copies.changed(oneWayCall.rooms));
203
+ if (oneWayCall.type === 'work_available' && oneWayCall.subject === 'agents')
204
+ logged(agents.refresh());
205
+ scripts.notified(oneWayCall);
206
+ },
194
207
  });
195
208
  }
package/dist/scope.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
3
3
  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
  }
@@ -119,15 +122,23 @@ function members(group) {
119
122
  }
120
123
  return pids;
121
124
  }
125
+ function besideCli(cli) {
126
+ try {
127
+ return `${realpathSync(cli)}-`;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
122
133
  export class Scope {
123
134
  marker;
124
- cli;
135
+ helpers;
125
136
  group;
126
137
  session = null;
127
138
  recorded = null;
128
139
  constructor(marker, name, cli) {
129
140
  this.marker = marker;
130
- this.cli = cli;
141
+ this.helpers = besideCli(cli);
131
142
  const own = ownGroup();
132
143
  this.group = own === null ? null : join(own, name);
133
144
  try {
@@ -165,7 +176,7 @@ export class Scope {
165
176
  return true;
166
177
  if (this.recorded === null)
167
178
  return found.size > 0;
168
- return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(`${this.cli}-`)));
179
+ return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || (this.helpers !== null && command.startsWith(this.helpers))));
169
180
  }
170
181
  empty() {
171
182
  return this.found()?.size === 0;
@@ -0,0 +1,255 @@
1
+ import { spawn as spawnPiped } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { once } from 'node:events';
4
+ import { rmSync } from 'node:fs';
5
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
6
+ import { constants } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { loginPath, onLoginPath } from './clis.js';
9
+ import { kitHome } from './home.js';
10
+ import { childEnvironment } from './relay.js';
11
+ import { killTree } from './scope.js';
12
+ const TERMINAL = 'xterm-256color';
13
+ const FIRST_SIZE = { cols: 80, rows: 24 };
14
+ const SIGNALLED = 128;
15
+ const NEWLINE = 0x0a;
16
+ const NO_BASH = "bash is not on the login shell's PATH";
17
+ const LOST = { refused: 'the Kit lost its connection to House' };
18
+ function causeOf(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ function failed(plan, cause) {
22
+ process.stderr.write(`kit: ${plan.named} could not start: ${cause}\n`);
23
+ return { refused: `the script could not start: ${cause}` };
24
+ }
25
+ function planned(call) {
26
+ if (call.kind === 'run_wizard')
27
+ return { named: 'a Web Wizard', script: call.script, terminal: 'page', function: null };
28
+ return {
29
+ named: 'an Environment function',
30
+ script: call.script,
31
+ terminal: call.terminal ? 'at once' : null,
32
+ function: { input: call.input, timeCap: call.time_cap, outputCap: call.output_cap },
33
+ };
34
+ }
35
+ function inputs() {
36
+ return join(kitHome(), 'inputs');
37
+ }
38
+ async function inputFile(value) {
39
+ await mkdir(inputs(), { recursive: true, mode: 0o700 });
40
+ const path = join(inputs(), `${randomUUID()}.json`);
41
+ await writeFile(path, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
42
+ return path;
43
+ }
44
+ export function removeInputs() {
45
+ rmSync(inputs(), { recursive: true, force: true });
46
+ }
47
+ function struck(line, typed) {
48
+ return typed.some((typedLine) => line.includes(typedLine));
49
+ }
50
+ class KeptOutput {
51
+ cap;
52
+ typedText = '';
53
+ text = '';
54
+ bytes = 0;
55
+ line = '';
56
+ lineBytes = 0;
57
+ dropping = false;
58
+ cut = false;
59
+ constructor(cap) {
60
+ this.cap = cap;
61
+ }
62
+ typed(data) {
63
+ this.typedText += data;
64
+ }
65
+ add(data) {
66
+ const pieces = data.split('\n');
67
+ const rest = pieces.pop();
68
+ for (const piece of pieces)
69
+ this.ended(piece);
70
+ this.extend(rest);
71
+ }
72
+ kept() {
73
+ const typed = this.typedLines(true);
74
+ this.text = [...this.text.split(/(?<=\n)/), this.line].filter((line) => line !== '' && !struck(line, typed)).join('');
75
+ this.bytes = Buffer.byteLength(this.text);
76
+ this.trim();
77
+ return { output: this.text, cut: this.cut };
78
+ }
79
+ typedLines(all) {
80
+ const lines = this.typedText.split(/\r\n|\r|\n/);
81
+ if (!all)
82
+ lines.pop();
83
+ return lines.filter((line) => line !== '');
84
+ }
85
+ ended(piece) {
86
+ const line = `${this.line}${piece}\n`;
87
+ const dropped = this.dropping;
88
+ this.line = '';
89
+ this.lineBytes = 0;
90
+ this.dropping = false;
91
+ if (dropped || struck(line, this.typedLines(false)))
92
+ return;
93
+ this.text += line;
94
+ this.bytes += Buffer.byteLength(line);
95
+ if (this.bytes > 2 * this.cap)
96
+ this.trim();
97
+ }
98
+ extend(piece) {
99
+ if (this.dropping)
100
+ return;
101
+ this.line += piece;
102
+ this.lineBytes += Buffer.byteLength(piece);
103
+ if (this.lineBytes <= this.cap)
104
+ return;
105
+ this.text = '';
106
+ this.bytes = 0;
107
+ this.line = '';
108
+ this.lineBytes = 0;
109
+ this.dropping = true;
110
+ this.cut = true;
111
+ }
112
+ trim() {
113
+ if (this.bytes <= this.cap)
114
+ return;
115
+ const bytes = Buffer.from(this.text);
116
+ const start = bytes.indexOf(NEWLINE, bytes.length - this.cap - 1) + 1;
117
+ this.text = bytes.subarray(start).toString('utf8');
118
+ this.bytes = bytes.length - start;
119
+ this.cut = true;
120
+ }
121
+ }
122
+ export class Scripts {
123
+ calls;
124
+ send;
125
+ runs = new Map();
126
+ constructor(calls, send) {
127
+ this.calls = calls;
128
+ this.send = send;
129
+ }
130
+ start(call) {
131
+ void this.calls.carry(call.input_id, () => this.run(call.input_id, planned(call)));
132
+ }
133
+ notified(oneWayCall) {
134
+ const run = this.runs.get(String(oneWayCall.wizard));
135
+ if (run === undefined)
136
+ return;
137
+ if (oneWayCall.type === 'wizard_input')
138
+ this.typed(run, String(oneWayCall.data));
139
+ if (oneWayCall.type === 'wizard_size')
140
+ this.resized(run, { cols: Number(oneWayCall.cols), rows: Number(oneWayCall.rows) });
141
+ if (oneWayCall.type === 'wizard_end' && run.plan.terminal === 'page')
142
+ this.end(run, {});
143
+ }
144
+ disconnected() {
145
+ for (const run of this.runs.values())
146
+ this.end(run, LOST);
147
+ }
148
+ async run(id, plan) {
149
+ const size = plan.terminal === 'page' ? null : FIRST_SIZE;
150
+ const output = plan.function === null ? null : new KeptOutput(plan.function.outputCap);
151
+ const run = { plan, input: null, output, process: null, pipes: [], terminal: null, size, typed: [], ended: null, sized: Promise.withResolvers() };
152
+ if (size !== null)
153
+ run.sized.resolve();
154
+ this.runs.set(id, run);
155
+ try {
156
+ return { result: await this.ran(id, run), release: plan.function !== null };
157
+ }
158
+ finally {
159
+ this.runs.delete(id);
160
+ if (run.input !== null)
161
+ await rm(run.input, { force: true });
162
+ }
163
+ }
164
+ async ran(id, run) {
165
+ const { plan, output } = run;
166
+ let exited;
167
+ try {
168
+ const { spawn } = await import('@lydell/node-pty');
169
+ const shell = await onLoginPath('bash');
170
+ if (shell === null)
171
+ return failed(plan, NO_BASH);
172
+ const path = await loginPath();
173
+ await run.sized.promise;
174
+ if (plan.function !== null)
175
+ run.input = await inputFile(plan.function.input);
176
+ if (run.ended !== null)
177
+ return run.ended;
178
+ const environment = { ...childEnvironment(), PATH: path, ...(run.input === null ? {} : { HOUSE_INPUT: run.input }) };
179
+ if (plan.terminal === null) {
180
+ ({ exited } = await this.piped(plan.script, shell, environment, run));
181
+ }
182
+ else {
183
+ const terminal = spawn(shell, ['-c', plan.script], { name: TERMINAL, ...run.size, cwd: process.cwd(), env: environment });
184
+ run.process = terminal;
185
+ run.terminal = terminal;
186
+ terminal.onData((data) => {
187
+ this.send({ type: 'wizard_output', wizard: id, data });
188
+ output?.add(data);
189
+ });
190
+ for (const data of run.typed.splice(0))
191
+ terminal.write(data);
192
+ exited = new Promise((resolve) => terminal.onExit(({ exitCode, signal }) => resolve(signal ? SIGNALLED + signal : exitCode)));
193
+ }
194
+ }
195
+ catch (error) {
196
+ return failed(plan, causeOf(error));
197
+ }
198
+ let timedOut = false;
199
+ // House sends a function run's time cap in seconds, and a run still going at its cap ends with its process group and the output pipes it holds.
200
+ const capped = plan.function === null ? undefined : setTimeout(() => {
201
+ timedOut = true;
202
+ this.stop(run);
203
+ }, plan.function.timeCap * 1000);
204
+ const code = await exited;
205
+ clearTimeout(capped);
206
+ if (run.ended !== null)
207
+ return run.ended;
208
+ if (output === null)
209
+ return { exit_code: code };
210
+ const kept = output.kept();
211
+ return timedOut ? { timed_out: true, ...kept } : { exit_code: code, ...kept };
212
+ }
213
+ async piped(script, shell, environment, run) {
214
+ const child = spawnPiped(shell, ['-c', script], { cwd: process.cwd(), env: environment, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
215
+ const exited = new Promise((resolve) => child.once('close', (code, signal) => resolve(signal === null ? code : SIGNALLED + constants.signals[signal])));
216
+ await once(child, 'spawn');
217
+ run.process = { pid: child.pid };
218
+ run.pipes = [child.stdout, child.stderr];
219
+ for (const pipe of run.pipes) {
220
+ pipe.setEncoding('utf8');
221
+ pipe.on('data', (data) => run.output.add(data));
222
+ }
223
+ if (run.ended !== null)
224
+ this.stop(run);
225
+ return { exited };
226
+ }
227
+ typed(run, data) {
228
+ run.output?.typed(data);
229
+ if (run.terminal === null)
230
+ run.typed.push(data);
231
+ else
232
+ run.terminal.write(data);
233
+ }
234
+ resized(run, size) {
235
+ run.size = size;
236
+ run.sized.resolve();
237
+ try {
238
+ run.terminal?.resize(size.cols, size.rows);
239
+ }
240
+ catch { }
241
+ }
242
+ end(run, result) {
243
+ if (run.ended !== null)
244
+ return;
245
+ run.ended = result;
246
+ run.sized.resolve();
247
+ this.stop(run);
248
+ }
249
+ stop(run) {
250
+ if (run.process !== null)
251
+ killTree(run.process);
252
+ for (const pipe of run.pipes)
253
+ pipe.destroy();
254
+ }
255
+ }
@@ -2,7 +2,7 @@ import { finalRefusal, retryDelay } from './api.js';
2
2
  export async function holdSecretInput(house, subject, steps, waiting) {
3
3
  for (let failures = 0; waiting();) {
4
4
  try {
5
- const held = await house.post(`/kit/secret-input/${subject}`, { steps });
5
+ const held = await house.post('/kit/secret-inputs/await', { subject, steps });
6
6
  if (held.outcome === 'collected')
7
7
  return held.content;
8
8
  if (held.release !== 'held')
package/dist/service.js CHANGED
@@ -1,4 +1,7 @@
1
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';
2
5
  const UNIT = 'house-kit.service';
3
6
  // launchctl and systemctl answer at once, so ten seconds only bounds a stuck service manager.
4
7
  const MANAGER_MS = 10_000;
@@ -10,7 +13,21 @@ function start(command, args) {
10
13
  if (started.status === 0)
11
14
  return;
12
15
  const cause = started.error?.message ?? (started.stderr.trim() || `${command} exited with ${started.status ?? 'a signal'}`);
13
- throw new Error(`the House Kit service could not be started: ${cause}`);
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());
14
31
  }
15
32
  export function startService() {
16
33
  if (process.platform === 'darwin') {
@@ -18,12 +35,17 @@ export function startService() {
18
35
  const printed = manage('launchctl', ['print', service]);
19
36
  if (printed.status !== 0 || /^\tstate = running$/m.test(printed.stdout))
20
37
  return false;
38
+ if (!runsThisHome(/^\t+HOUSE_KIT_HOME => (.+)$/m.exec(printed.stdout)?.[1]))
39
+ return false;
21
40
  start('launchctl', ['kickstart', service]);
22
41
  return true;
23
42
  }
24
43
  for (const scope of [['--user'], []]) {
25
44
  if (manage('systemctl', [...scope, 'is-enabled', '--quiet', UNIT]).status !== 0)
26
45
  continue;
46
+ const environment = manage('systemctl', [...scope, 'show', '--property=Environment', '--value', UNIT]).stdout;
47
+ if (!runsThisHome(systemdHome(environment)))
48
+ continue;
27
49
  if (manage('systemctl', [...scope, 'is-active', '--quiet', UNIT]).status === 0)
28
50
  return false;
29
51
  start('systemctl', [...scope, 'start', UNIT]);
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
  }
@@ -0,0 +1,28 @@
1
+ const SYSTEM_CLOCK = {
2
+ later: (delay, callback) => setTimeout(callback, delay),
3
+ clear: (timer) => clearTimeout(timer),
4
+ };
5
+ export class Silence {
6
+ timer = null;
7
+ interval;
8
+ silent;
9
+ clock;
10
+ constructor(interval, silent, clock = SYSTEM_CLOCK) {
11
+ this.interval = interval;
12
+ this.silent = silent;
13
+ this.clock = clock;
14
+ }
15
+ heard() {
16
+ this.stop();
17
+ this.timer = this.clock.later(this.interval, () => {
18
+ this.timer = null;
19
+ this.silent();
20
+ });
21
+ }
22
+ stop() {
23
+ if (this.timer === null)
24
+ return;
25
+ this.clock.clear(this.timer);
26
+ this.timer = null;
27
+ }
28
+ }