@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c3e86aacccc80
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.
- package/README.md +21 -20
- package/dist/acp.js +7 -97
- package/dist/agents.js +3 -2
- package/dist/api.js +5 -5
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +11 -37
- package/dist/clis.js +52 -13
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +55 -55
- package/dist/copies.js +127 -102
- package/dist/end.js +14 -4
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +10 -76
- package/dist/home.js +41 -3
- package/dist/house-main.js +76 -32
- package/dist/instructions.js +7 -5
- package/dist/login.js +89 -105
- package/dist/logout.js +14 -14
- package/dist/parts.js +34 -16
- package/dist/relay.js +12 -2
- package/dist/resident.js +27 -20
- package/dist/scope.js +203 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +33 -0
- package/dist/sign-in.js +1 -1
- package/dist/stream.js +5 -3
- package/dist/work.js +348 -0
- package/package.json +2 -2
package/dist/scope.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const LISTING_BYTES = 1 << 28;
|
|
5
|
+
const CGROUPS = '/sys/fs/cgroup';
|
|
6
|
+
const GONE = new Set(['ENOENT', 'ESRCH']);
|
|
7
|
+
function failure(error) {
|
|
8
|
+
return error.code ?? '';
|
|
9
|
+
}
|
|
10
|
+
function listed(environment) {
|
|
11
|
+
const found = new Map();
|
|
12
|
+
const output = execFileSync('ps', [environment ? '-AEww' : '-Aww', '-o', 'pid=,ppid=,sess=,command='], {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: LISTING_BYTES,
|
|
15
|
+
});
|
|
16
|
+
for (const line of output.split('\n')) {
|
|
17
|
+
const fields = /^\s*(\d+)\s+(\d+)\s+(\S+)\s?(.*)$/.exec(line);
|
|
18
|
+
if (fields !== null)
|
|
19
|
+
found.set(Number(fields[1]), { parent: Number(fields[2]), session: fields[3], command: fields[4] });
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
function read(pid) {
|
|
24
|
+
try {
|
|
25
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
26
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
27
|
+
const command = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replaceAll('\0', ' ');
|
|
28
|
+
return { parent: Number(fields[1]), session: fields[3], command };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (GONE.has(failure(error)))
|
|
32
|
+
return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function processes() {
|
|
37
|
+
if (process.platform === 'darwin')
|
|
38
|
+
return listed(false);
|
|
39
|
+
const found = new Map();
|
|
40
|
+
for (const entry of readdirSync('/proc')) {
|
|
41
|
+
const running = /^\d+$/.test(entry) ? read(entry) : null;
|
|
42
|
+
if (running !== null)
|
|
43
|
+
found.set(Number(entry), running);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function descendants(roots, table = processes()) {
|
|
48
|
+
const children = new Map();
|
|
49
|
+
for (const [pid, { parent }] of table)
|
|
50
|
+
children.set(parent, [...(children.get(parent) ?? []), pid]);
|
|
51
|
+
const tree = [];
|
|
52
|
+
const pending = roots.flatMap((root) => children.get(root) ?? []);
|
|
53
|
+
while (pending.length > 0) {
|
|
54
|
+
const pid = pending.pop();
|
|
55
|
+
tree.push(pid);
|
|
56
|
+
pending.push(...(children.get(pid) ?? []));
|
|
57
|
+
}
|
|
58
|
+
return tree;
|
|
59
|
+
}
|
|
60
|
+
function marked(marker, doubted) {
|
|
61
|
+
if (process.platform === 'darwin') {
|
|
62
|
+
return [...listed(true)].filter(([, { command }]) => ` ${command} `.includes(` ${marker} `)).map(([pid]) => pid);
|
|
63
|
+
}
|
|
64
|
+
return readdirSync('/proc')
|
|
65
|
+
.filter((entry) => /^\d+$/.test(entry) && carries(entry, marker, doubted))
|
|
66
|
+
.map(Number);
|
|
67
|
+
}
|
|
68
|
+
function carries(pid, marker, doubted) {
|
|
69
|
+
try {
|
|
70
|
+
return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').includes(marker);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (GONE.has(failure(error)))
|
|
74
|
+
return false;
|
|
75
|
+
if (failure(error) !== 'EACCES')
|
|
76
|
+
throw error;
|
|
77
|
+
return doubted && statSync(`/proc/${pid}`, { throwIfNoEntry: false })?.uid === process.getuid();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function killAll(pids) {
|
|
81
|
+
for (const pid of pids) {
|
|
82
|
+
for (const target of [-pid, pid]) {
|
|
83
|
+
try {
|
|
84
|
+
process.kill(target, 'SIGKILL');
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function killTree(child) {
|
|
91
|
+
killAll([child.pid, ...descendants([child.pid])]);
|
|
92
|
+
}
|
|
93
|
+
export function killMarked(marker) {
|
|
94
|
+
const pids = marked(marker, false);
|
|
95
|
+
killAll([...pids, ...descendants(pids)]);
|
|
96
|
+
}
|
|
97
|
+
export function killDescendants() {
|
|
98
|
+
const pids = marked(`HOUSE_KIT_RESIDENT=${process.pid}`, false);
|
|
99
|
+
killAll([...pids, ...descendants([process.pid, ...pids])]);
|
|
100
|
+
}
|
|
101
|
+
function ownGroup() {
|
|
102
|
+
if (process.platform !== 'linux')
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
const line = readFileSync('/proc/self/cgroup', 'utf8')
|
|
106
|
+
.split('\n')
|
|
107
|
+
.find((entry) => entry.startsWith('0::'));
|
|
108
|
+
return line === undefined ? null : join(CGROUPS, line.slice(3));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function members(group) {
|
|
115
|
+
const pids = readFileSync(join(group, 'cgroup.procs'), 'utf8').split('\n').filter(Boolean).map(Number);
|
|
116
|
+
for (const entry of readdirSync(group, { withFileTypes: true })) {
|
|
117
|
+
if (entry.isDirectory())
|
|
118
|
+
pids.push(...members(join(group, entry.name)));
|
|
119
|
+
}
|
|
120
|
+
return pids;
|
|
121
|
+
}
|
|
122
|
+
export class Scope {
|
|
123
|
+
marker;
|
|
124
|
+
cli;
|
|
125
|
+
group;
|
|
126
|
+
session = null;
|
|
127
|
+
recorded = null;
|
|
128
|
+
constructor(marker, name, cli) {
|
|
129
|
+
this.marker = marker;
|
|
130
|
+
this.cli = cli;
|
|
131
|
+
const own = ownGroup();
|
|
132
|
+
this.group = own === null ? null : join(own, name);
|
|
133
|
+
try {
|
|
134
|
+
if (this.group !== null)
|
|
135
|
+
mkdirSync(this.group);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
this.group = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
command(command, args) {
|
|
142
|
+
if (this.group === null)
|
|
143
|
+
return [command, args];
|
|
144
|
+
return ['/bin/sh', ['-c', 'echo $$ > "$0/cgroup.procs" 2>/dev/null; exec "$@"', this.group, command, ...args]];
|
|
145
|
+
}
|
|
146
|
+
entered(pid) {
|
|
147
|
+
this.session = process.platform === 'darwin' ? (listed(false).get(pid)?.session ?? null) : String(pid);
|
|
148
|
+
if (this.group === null)
|
|
149
|
+
return;
|
|
150
|
+
const joined = readFileSync(`/proc/${pid}/cgroup`, 'utf8').split('\n').find((entry) => entry.startsWith('0::'));
|
|
151
|
+
if (joined !== undefined && join(CGROUPS, joined.slice(3)) === this.group)
|
|
152
|
+
return;
|
|
153
|
+
this.close();
|
|
154
|
+
this.group = null;
|
|
155
|
+
}
|
|
156
|
+
record() {
|
|
157
|
+
const found = this.found();
|
|
158
|
+
if (found === null)
|
|
159
|
+
return;
|
|
160
|
+
this.recorded = new Set([...found].filter(([, { session }]) => session === this.session).map(([pid]) => pid));
|
|
161
|
+
}
|
|
162
|
+
working() {
|
|
163
|
+
const found = this.found();
|
|
164
|
+
if (found === null)
|
|
165
|
+
return true;
|
|
166
|
+
if (this.recorded === null)
|
|
167
|
+
return found.size > 0;
|
|
168
|
+
return [...found].some(([pid, { session, command }]) => session !== this.session || !(this.recorded.has(pid) || command.startsWith(`${this.cli}-`)));
|
|
169
|
+
}
|
|
170
|
+
empty() {
|
|
171
|
+
return this.found()?.size === 0;
|
|
172
|
+
}
|
|
173
|
+
kill() {
|
|
174
|
+
killMarked(this.marker);
|
|
175
|
+
if (this.group === null)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
writeFileSync(join(this.group, 'cgroup.kill'), '1');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
killAll([...(this.found()?.keys() ?? [])]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
close() {
|
|
185
|
+
if (this.group === null)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
rmdirSync(this.group);
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
found() {
|
|
193
|
+
try {
|
|
194
|
+
const table = processes();
|
|
195
|
+
const roots = this.group === null ? marked(this.marker, true) : members(this.group);
|
|
196
|
+
const pids = this.group === null ? [...roots, ...descendants(roots, table)] : roots;
|
|
197
|
+
return new Map(pids.flatMap((pid) => (table.has(pid) ? [[pid, table.get(pid)]] : [])));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
const UNIT = 'house-kit.service';
|
|
3
|
+
// launchctl and systemctl answer at once, so ten seconds only bounds a stuck service manager.
|
|
4
|
+
const MANAGER_MS = 10_000;
|
|
5
|
+
function manage(command, args) {
|
|
6
|
+
return spawnSync(command, args, { encoding: 'utf8', timeout: MANAGER_MS });
|
|
7
|
+
}
|
|
8
|
+
function start(command, args) {
|
|
9
|
+
const started = manage(command, args);
|
|
10
|
+
if (started.status === 0)
|
|
11
|
+
return;
|
|
12
|
+
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}`);
|
|
14
|
+
}
|
|
15
|
+
export function startService() {
|
|
16
|
+
if (process.platform === 'darwin') {
|
|
17
|
+
const service = `gui/${process.getuid()}/house-kit`;
|
|
18
|
+
const printed = manage('launchctl', ['print', service]);
|
|
19
|
+
if (printed.status !== 0 || /^\tstate = running$/m.test(printed.stdout))
|
|
20
|
+
return false;
|
|
21
|
+
start('launchctl', ['kickstart', service]);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
for (const scope of [['--user'], []]) {
|
|
25
|
+
if (manage('systemctl', [...scope, 'is-enabled', '--quiet', UNIT]).status !== 0)
|
|
26
|
+
continue;
|
|
27
|
+
if (manage('systemctl', [...scope, 'is-active', '--quiet', UNIT]).status === 0)
|
|
28
|
+
return false;
|
|
29
|
+
start('systemctl', [...scope, 'start', UNIT]);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
package/dist/sign-in.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { killTree } from './acp.js';
|
|
4
3
|
import { CLIS, withoutProxy } from './clis.js';
|
|
4
|
+
import { killTree } from './scope.js';
|
|
5
5
|
import { holdSecretInput } from './secret-input.js';
|
|
6
6
|
const ESCAPE = /\u001b\[[\d;]*m/g;
|
|
7
7
|
const LINK = /https:\/\/\S+/;
|
package/dist/stream.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { subscribe } from 'node:diagnostics_channel';
|
|
2
2
|
import { finalRefusal, HouseRefusal, retryDelay } from './api.js';
|
|
3
|
-
import { end, REPLACED,
|
|
3
|
+
import { end, REPLACED, signInAgain } from './end.js';
|
|
4
4
|
import { readEnrolment } from './home.js';
|
|
5
5
|
import { relayed } from './relay.js';
|
|
6
6
|
const PATH = '/kit/stream';
|
|
@@ -52,8 +52,10 @@ export function holdStream(handlers) {
|
|
|
52
52
|
process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
|
|
53
53
|
if (event.reason === 'replaced')
|
|
54
54
|
end(REPLACED);
|
|
55
|
-
if (event.reason === 'credential_rejected' || refused?.status === 401)
|
|
56
|
-
|
|
55
|
+
if (event.reason === 'credential_rejected' || refused?.status === 401) {
|
|
56
|
+
void signInAgain(credential);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
57
59
|
if (finalRefusal(refused))
|
|
58
60
|
end(refused.message);
|
|
59
61
|
// A closed socket reopens in one to three seconds, spread so Kits one restart closed return apart; an opening House refused as retryable or left unanswered backs off on the jittered curve.
|
package/dist/work.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
5
|
+
import { createServer } from 'node:net';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { RUNNING_JOB } from './events.js';
|
|
8
|
+
import { kitHome } from './home.js';
|
|
9
|
+
const ENDED_TASK = new Set(['completed', 'failed', 'killed']);
|
|
10
|
+
const SCHEDULING = new Set(['CronCreate', 'CronDelete', 'ScheduleWakeup']);
|
|
11
|
+
const QUEUE_LIST = 'house-kit-queue-';
|
|
12
|
+
const SYNC = 'house-kit-sync-';
|
|
13
|
+
const WAKEUP = /Next wakeup scheduled for (\d\d):(\d\d):(\d\d) \(in (\d+)s\)/;
|
|
14
|
+
const HALF_DAY = 12 * 60 * 60 * 1000;
|
|
15
|
+
export function codexLauncher() {
|
|
16
|
+
return join(kitHome(), 'launchers', 'codex');
|
|
17
|
+
}
|
|
18
|
+
export class Work {
|
|
19
|
+
changed;
|
|
20
|
+
jobs = new Set();
|
|
21
|
+
constructor(changed) {
|
|
22
|
+
this.changed = changed;
|
|
23
|
+
}
|
|
24
|
+
async open(_cli) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
async settled() { }
|
|
28
|
+
read(message) {
|
|
29
|
+
this.take(message);
|
|
30
|
+
this.changed();
|
|
31
|
+
}
|
|
32
|
+
take(message) {
|
|
33
|
+
const update = message.params?.update;
|
|
34
|
+
if (message.method !== 'session/update' || update?.asyncTaskId === undefined)
|
|
35
|
+
return;
|
|
36
|
+
if (update.sessionUpdate === 'async_task_spawned')
|
|
37
|
+
this.jobs.add(update.asyncTaskId);
|
|
38
|
+
if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state))
|
|
39
|
+
this.jobs.delete(update.asyncTaskId);
|
|
40
|
+
}
|
|
41
|
+
busy() {
|
|
42
|
+
return this.jobs.size > 0;
|
|
43
|
+
}
|
|
44
|
+
close() { }
|
|
45
|
+
}
|
|
46
|
+
class ClaudeWork extends Work {
|
|
47
|
+
cwd;
|
|
48
|
+
level = new Set();
|
|
49
|
+
listed = new Set();
|
|
50
|
+
started = new Set();
|
|
51
|
+
running = false;
|
|
52
|
+
hooks = new Set();
|
|
53
|
+
calls = new Map();
|
|
54
|
+
crons = new Map();
|
|
55
|
+
wakeups = [];
|
|
56
|
+
goal = false;
|
|
57
|
+
constructor(cwd, changed) {
|
|
58
|
+
super(changed);
|
|
59
|
+
this.cwd = cwd;
|
|
60
|
+
}
|
|
61
|
+
take(message) {
|
|
62
|
+
super.take(message);
|
|
63
|
+
if (message.method === '_claude/sdkMessage')
|
|
64
|
+
this.sdk(message.params.message);
|
|
65
|
+
if (message.method === 'session/update')
|
|
66
|
+
this.update(message.params.update);
|
|
67
|
+
}
|
|
68
|
+
busy() {
|
|
69
|
+
return (super.busy() ||
|
|
70
|
+
this.level.size > 0 ||
|
|
71
|
+
this.started.size > 0 ||
|
|
72
|
+
this.running ||
|
|
73
|
+
this.hooks.size > 0 ||
|
|
74
|
+
this.crons.size > 0 ||
|
|
75
|
+
this.wakeups.length > 0 ||
|
|
76
|
+
this.goal ||
|
|
77
|
+
this.durable());
|
|
78
|
+
}
|
|
79
|
+
sdk(message) {
|
|
80
|
+
switch (message.subtype) {
|
|
81
|
+
case 'background_tasks_changed':
|
|
82
|
+
this.level = new Set(message.tasks.map((task) => task.task_id));
|
|
83
|
+
for (const id of this.level) {
|
|
84
|
+
this.listed.add(id);
|
|
85
|
+
this.started.delete(id);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'task_started':
|
|
89
|
+
if (!this.listed.has(message.task_id))
|
|
90
|
+
this.started.add(message.task_id);
|
|
91
|
+
break;
|
|
92
|
+
case 'task_updated':
|
|
93
|
+
if (ENDED_TASK.has(message.patch.status))
|
|
94
|
+
this.started.delete(message.task_id);
|
|
95
|
+
break;
|
|
96
|
+
case 'task_notification':
|
|
97
|
+
this.started.delete(message.task_id);
|
|
98
|
+
break;
|
|
99
|
+
case 'session_state_changed':
|
|
100
|
+
if (message.state === 'running' && !this.running)
|
|
101
|
+
this.wakeups = this.wakeups.filter((due) => due > Date.now());
|
|
102
|
+
this.running = message.state === 'running';
|
|
103
|
+
break;
|
|
104
|
+
case 'hook_started':
|
|
105
|
+
this.hooks.add(message.hook_id);
|
|
106
|
+
break;
|
|
107
|
+
case 'hook_response':
|
|
108
|
+
this.hooks.delete(message.hook_id);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
update(update) {
|
|
113
|
+
const air = update._meta?.jetbrains?.air;
|
|
114
|
+
if (update.sessionUpdate === 'session_info_update' && air !== undefined && 'goal' in air) {
|
|
115
|
+
this.goal = air.goal?.status === 'active';
|
|
116
|
+
}
|
|
117
|
+
if (update.sessionUpdate !== 'tool_call' && update.sessionUpdate !== 'tool_call_update')
|
|
118
|
+
return;
|
|
119
|
+
const tool = update._meta?.claudeCode?.toolName;
|
|
120
|
+
if (update.sessionUpdate === 'tool_call' && SCHEDULING.has(tool ?? ''))
|
|
121
|
+
this.calls.set(update.toolCallId, { tool });
|
|
122
|
+
const call = this.calls.get(update.toolCallId);
|
|
123
|
+
if (call === undefined)
|
|
124
|
+
return;
|
|
125
|
+
if (update.rawInput !== undefined)
|
|
126
|
+
call.input = update.rawInput;
|
|
127
|
+
if (update.content !== undefined)
|
|
128
|
+
call.content = update.content;
|
|
129
|
+
if (update.status === 'failed')
|
|
130
|
+
this.calls.delete(update.toolCallId);
|
|
131
|
+
if (update.status !== 'completed')
|
|
132
|
+
return;
|
|
133
|
+
this.calls.delete(update.toolCallId);
|
|
134
|
+
if (call.tool === 'CronCreate')
|
|
135
|
+
this.crons.set(update.toolCallId, JSON.stringify(call.content ?? []));
|
|
136
|
+
if (call.tool === 'ScheduleWakeup')
|
|
137
|
+
this.wakeup(call);
|
|
138
|
+
if (call.tool !== 'CronDelete')
|
|
139
|
+
return;
|
|
140
|
+
for (const [id, text] of this.crons)
|
|
141
|
+
if (text.includes(String(call.input?.id)))
|
|
142
|
+
this.crons.delete(id);
|
|
143
|
+
}
|
|
144
|
+
wakeup(call) {
|
|
145
|
+
if (call.input?.stop === true) {
|
|
146
|
+
this.wakeups = [];
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const time = WAKEUP.exec(JSON.stringify(call.content ?? []));
|
|
150
|
+
if (time === null) {
|
|
151
|
+
this.wakeups.push(Infinity);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const fires = Date.now() + Number(time[4]) * 1000;
|
|
155
|
+
const due = new Date(fires);
|
|
156
|
+
due.setHours(Number(time[1]), Number(time[2]), Number(time[3]), 0);
|
|
157
|
+
if (due.getTime() - fires > HALF_DAY)
|
|
158
|
+
due.setDate(due.getDate() - 1);
|
|
159
|
+
if (fires - due.getTime() > HALF_DAY)
|
|
160
|
+
due.setDate(due.getDate() + 1);
|
|
161
|
+
this.wakeups.push(due.getTime());
|
|
162
|
+
}
|
|
163
|
+
durable() {
|
|
164
|
+
let text;
|
|
165
|
+
try {
|
|
166
|
+
text = readFileSync(join(this.cwd, '.claude', 'scheduled_tasks.json'), 'utf8');
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return error.code !== 'ENOENT';
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(text).tasks?.length !== 0;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
class CodexWork extends Work {
|
|
180
|
+
threads = new Map();
|
|
181
|
+
queues = new Map();
|
|
182
|
+
goals = new Set();
|
|
183
|
+
items = new Set();
|
|
184
|
+
hooks = new Set();
|
|
185
|
+
starting = new Set();
|
|
186
|
+
settling = [];
|
|
187
|
+
memories = null;
|
|
188
|
+
lost = false;
|
|
189
|
+
server = null;
|
|
190
|
+
path = null;
|
|
191
|
+
socket = null;
|
|
192
|
+
asked = 0;
|
|
193
|
+
heard = 0;
|
|
194
|
+
proving = -1;
|
|
195
|
+
proven = -1;
|
|
196
|
+
async open(cli) {
|
|
197
|
+
const sockets = join(kitHome(), 'bridges');
|
|
198
|
+
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
199
|
+
this.path = join(sockets, `${randomUUID()}.sock`);
|
|
200
|
+
this.server = createServer((socket) => this.connected(socket));
|
|
201
|
+
this.server.listen(this.path);
|
|
202
|
+
await once(this.server, 'listening');
|
|
203
|
+
return { CODEX_PATH: codexLauncher(), HOUSE_KIT_CODEX: cli, HOUSE_KIT_CODEX_STREAM: this.path };
|
|
204
|
+
}
|
|
205
|
+
settled() {
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
this.settling.push(resolve);
|
|
208
|
+
this.settle();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
take(message) {
|
|
212
|
+
super.take(message);
|
|
213
|
+
this.heard++;
|
|
214
|
+
}
|
|
215
|
+
busy() {
|
|
216
|
+
if (super.busy() ||
|
|
217
|
+
this.lost ||
|
|
218
|
+
this.memories !== false ||
|
|
219
|
+
[...this.threads.values()].some(Boolean) ||
|
|
220
|
+
this.queues.size > 0 ||
|
|
221
|
+
this.goals.size > 0 ||
|
|
222
|
+
this.items.size > 0 ||
|
|
223
|
+
this.hooks.size > 0) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (this.proven === this.heard)
|
|
227
|
+
return false;
|
|
228
|
+
this.prove();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
close() {
|
|
232
|
+
this.server?.close();
|
|
233
|
+
if (this.path !== null)
|
|
234
|
+
void rm(this.path, { force: true });
|
|
235
|
+
}
|
|
236
|
+
connected(socket) {
|
|
237
|
+
this.socket = socket;
|
|
238
|
+
let buffer = '';
|
|
239
|
+
socket.on('error', () => this.lose());
|
|
240
|
+
socket.on('close', () => this.lose());
|
|
241
|
+
socket.setEncoding('utf8').on('data', (chunk) => {
|
|
242
|
+
buffer += chunk;
|
|
243
|
+
for (let index = buffer.indexOf('\n'); index >= 0; index = buffer.indexOf('\n')) {
|
|
244
|
+
const message = this.parsed(buffer.slice(0, index));
|
|
245
|
+
buffer = buffer.slice(index + 1);
|
|
246
|
+
if (message !== null)
|
|
247
|
+
this.stream(message, socket);
|
|
248
|
+
}
|
|
249
|
+
this.settle();
|
|
250
|
+
this.changed();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
parsed(line) {
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(line);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
this.lost = true;
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
lose() {
|
|
263
|
+
this.lost = true;
|
|
264
|
+
this.changed();
|
|
265
|
+
}
|
|
266
|
+
prove() {
|
|
267
|
+
if (this.proving === this.heard)
|
|
268
|
+
return;
|
|
269
|
+
this.proving = this.heard;
|
|
270
|
+
this.socket.write(`${JSON.stringify({ jsonrpc: '2.0', id: `${SYNC}${this.heard}`, method: 'house-kit/sync' })}\n`);
|
|
271
|
+
}
|
|
272
|
+
settle() {
|
|
273
|
+
if (this.starting.size === 0)
|
|
274
|
+
for (const resolve of this.settling.splice(0))
|
|
275
|
+
resolve();
|
|
276
|
+
}
|
|
277
|
+
stream(message, socket) {
|
|
278
|
+
const params = message.params;
|
|
279
|
+
switch (message.method) {
|
|
280
|
+
case 'thread/status/changed':
|
|
281
|
+
this.threads.set(params.threadId, params.status.type === 'active' && params.status.activeFlags.length === 0);
|
|
282
|
+
break;
|
|
283
|
+
case 'thread/closed':
|
|
284
|
+
this.threads.delete(params.threadId);
|
|
285
|
+
break;
|
|
286
|
+
case 'thread/queue/changed':
|
|
287
|
+
this.queues.set(params.threadId, ++this.asked);
|
|
288
|
+
socket.write(`${JSON.stringify({ jsonrpc: '2.0', id: `${QUEUE_LIST}${this.asked}`, method: 'thread/queue/list', params: { threadId: params.threadId } })}\n`);
|
|
289
|
+
break;
|
|
290
|
+
case 'thread/goal/updated':
|
|
291
|
+
if (params.goal.status === 'active')
|
|
292
|
+
this.goals.add(params.threadId);
|
|
293
|
+
else
|
|
294
|
+
this.goals.delete(params.threadId);
|
|
295
|
+
break;
|
|
296
|
+
case 'thread/goal/cleared':
|
|
297
|
+
this.goals.delete(params.threadId);
|
|
298
|
+
break;
|
|
299
|
+
case 'item/started':
|
|
300
|
+
this.items.add(`${params.threadId} ${params.item.id}`);
|
|
301
|
+
break;
|
|
302
|
+
case 'item/completed':
|
|
303
|
+
this.items.delete(`${params.threadId} ${params.item.id}`);
|
|
304
|
+
break;
|
|
305
|
+
case 'hook/started':
|
|
306
|
+
this.hooks.add(params.run.id);
|
|
307
|
+
break;
|
|
308
|
+
case 'hook/completed':
|
|
309
|
+
this.hooks.delete(params.run.id);
|
|
310
|
+
break;
|
|
311
|
+
case 'mcpServer/startupStatus/updated':
|
|
312
|
+
if (params.status === 'starting')
|
|
313
|
+
this.starting.add(`${params.threadId} ${params.name}`);
|
|
314
|
+
else
|
|
315
|
+
this.starting.delete(`${params.threadId} ${params.name}`);
|
|
316
|
+
break;
|
|
317
|
+
case undefined:
|
|
318
|
+
this.answered(message);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
answered(message) {
|
|
322
|
+
if (typeof message.id === 'string' && message.id.startsWith(SYNC)) {
|
|
323
|
+
this.proven = Number(message.id.slice(SYNC.length));
|
|
324
|
+
}
|
|
325
|
+
else if (typeof message.id === 'string' && message.id.startsWith(QUEUE_LIST)) {
|
|
326
|
+
const asked = Number(message.id.slice(QUEUE_LIST.length));
|
|
327
|
+
for (const [thread, latest] of this.queues) {
|
|
328
|
+
if (latest === asked && message.result?.data?.length === 0)
|
|
329
|
+
this.queues.delete(thread);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (message.result?.config?.features !== undefined) {
|
|
333
|
+
this.memories = message.result.config.features.memories !== false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
class GrokWork extends Work {
|
|
338
|
+
busy() {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export function workFor(kind, cwd, changed) {
|
|
343
|
+
if (kind === 'claude-agent-acp')
|
|
344
|
+
return new ClaudeWork(cwd, changed);
|
|
345
|
+
if (kind === 'codex-acp')
|
|
346
|
+
return new CodexWork(changed);
|
|
347
|
+
return new GrokWork(changed);
|
|
348
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/kit",
|
|
3
|
-
"version": "0.2.1-
|
|
3
|
+
"version": "0.2.1-c3e86aacccc80",
|
|
4
4
|
"description": "House Kit connects an Environment to agents.house.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@agentclientprotocol/sdk": "1.7.0",
|
|
30
|
-
"@agentshouse/mdmodel": "0.1.0-
|
|
30
|
+
"@agentshouse/mdmodel": "0.1.0-c68e2039f1ca8",
|
|
31
31
|
"yaml": "2.9.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|