@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c5262a8cc063d

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/dist/stream.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { retryDelay } from './api.js';
1
2
  import { readEnrolment } from './home.js';
2
3
  const SUBPROTOCOL = 'house.kit.stream.1';
3
4
  function streamUrl(house) {
@@ -29,9 +30,7 @@ export function holdStream(handlers) {
29
30
  if (socket === opening)
30
31
  socket = null;
31
32
  process.stderr.write(`kit: control stream closed ${event.code} ${event.reason}\n`);
32
- const delay = opened
33
- ? 1000 + Math.random() * 2000
34
- : Math.min(30_000, 1000 * 2 ** failures++);
33
+ const delay = opened ? 1000 + Math.random() * 2000 : retryDelay(failures++);
35
34
  setTimeout(() => void connect(), delay);
36
35
  };
37
36
  };
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat } from 'node:fs/promises';
4
+ import { availableParallelism, tmpdir } from 'node:os';
5
+ import { basename, join } from 'node:path';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { unlessAborted } from './abort.js';
8
+ import { failureOf, run } from './clis.js';
9
+ import { kitHome } from './home.js';
10
+ const RELEASE = 'https://github.com/agentshouse/kit/releases/download/engine-2';
11
+ export const MODEL = {
12
+ url: 'https://huggingface.co/ggml-org/parakeet-GGUF/resolve/35156454d1a39de06863303dd209fd2bed6ee079/ggml-parakeet-tdt-0.6b-v3-q4_k.bin',
13
+ sha256: '8b205b8b39c6535e153de6fb11c51db46125d45c4f16ba496fe41a0fe71b885e',
14
+ executable: false,
15
+ };
16
+ export const ENGINES = {
17
+ 'linux-x64': {
18
+ decoder: { url: `${RELEASE}/ffmpeg-linux-x86_64`, sha256: '1f8235d206428305afb89c6e8ab99d100425a0ba61e22bb4c7aff017db8c8de5', executable: true },
19
+ engine: { url: `${RELEASE}/parakeet-cli-linux-x86_64`, sha256: 'bb2585695992fe1927a33960002cdf51b8a90a96ad65593c3d0a9f1fbc0865c2', executable: true },
20
+ },
21
+ 'linux-arm64': {
22
+ decoder: { url: `${RELEASE}/ffmpeg-linux-aarch64`, sha256: 'a7e08a0fae0179d1078312f6bb13d2a9aa4d4c71a1d82a022953f25dee452f5e', executable: true },
23
+ engine: { url: `${RELEASE}/parakeet-cli-linux-aarch64`, sha256: 'cbc0bedc8daae97f1ef9dcee4200c06226218e300e0fd62ead1306d22742b7ed', executable: true },
24
+ },
25
+ 'darwin-arm64': {
26
+ decoder: { url: `${RELEASE}/ffmpeg-darwin-arm64`, sha256: '7abe154aaed3d6b49f5167ca7221bb66bed51388925d99a7e4261a41fe52fba6', executable: true },
27
+ engine: { url: `${RELEASE}/parakeet-cli-darwin-arm64`, sha256: '9a2aafc0922ad7c0b6db6eff1641e96572ba32ac40bf995a1df242ed2febf297', executable: true },
28
+ },
29
+ };
30
+ const CHUNK_SECONDS = 20;
31
+ let held = Promise.resolve();
32
+ function directory() {
33
+ return join(kitHome(), 'transcription');
34
+ }
35
+ function installed(artifact) {
36
+ return join(directory(), artifact.sha256);
37
+ }
38
+ async function executed(command, args, signal) {
39
+ const ran = await run(command, args, 0, false, signal);
40
+ if (ran.status !== 0)
41
+ throw new Error(failureOf(ran, basename(command)));
42
+ return ran.stdout;
43
+ }
44
+ function present(path) {
45
+ return stat(path).then(() => true, () => false);
46
+ }
47
+ async function cpus() {
48
+ const [quota, period] = (await readFile('/sys/fs/cgroup/cpu.max', 'utf8').catch(() => 'max')).split(' ');
49
+ const allowed = quota === 'max' ? Infinity : Math.ceil(Number(quota) / Number(period));
50
+ return Math.min(availableParallelism(), allowed);
51
+ }
52
+ async function sha256(path) {
53
+ const hash = createHash('sha256');
54
+ await pipeline(createReadStream(path), hash);
55
+ return hash.digest('hex');
56
+ }
57
+ async function install(artifacts, signal) {
58
+ await mkdir(directory(), { recursive: true });
59
+ const pinned = new Set(artifacts.map((artifact) => artifact.sha256));
60
+ for (const name of await readdir(directory())) {
61
+ if (!pinned.has(name))
62
+ await rm(join(directory(), name), { recursive: true, force: true });
63
+ }
64
+ const missing = [];
65
+ for (const artifact of artifacts)
66
+ if (!(await present(installed(artifact))))
67
+ missing.push(artifact);
68
+ try {
69
+ for (const artifact of missing) {
70
+ const part = `${installed(artifact)}.part`;
71
+ await executed('curl', ['-fsSL', '--retry', '3', '-o', part, artifact.url], signal);
72
+ if ((await sha256(part)) !== artifact.sha256)
73
+ throw new Error(`${artifact.url} does not match its sha256`);
74
+ }
75
+ for (const artifact of missing) {
76
+ if (artifact.executable)
77
+ await chmod(`${installed(artifact)}.part`, 0o755);
78
+ await rename(`${installed(artifact)}.part`, installed(artifact));
79
+ }
80
+ }
81
+ finally {
82
+ for (const artifact of missing)
83
+ await rm(`${installed(artifact)}.part`, { force: true });
84
+ }
85
+ }
86
+ async function transcribed(path, signal) {
87
+ const platform = `${process.platform}-${process.arch}`;
88
+ const pinned = ENGINES[platform];
89
+ if (pinned === undefined)
90
+ throw new Error(`no transcription engine is pinned for ${platform}`);
91
+ await install([pinned.decoder, pinned.engine, MODEL], signal);
92
+ const work = await mkdtemp(join(tmpdir(), 'kit-transcription-'));
93
+ try {
94
+ await executed(installed(pinned.decoder), ['-nostdin', '-hide_banner', '-loglevel', 'error', '-i', path, '-ar', '16000', '-ac', '1', '-c:a', 'pcm_s16le',
95
+ '-f', 'segment', '-segment_time', String(CHUNK_SECONDS), join(work, '%05d.wav')], signal);
96
+ const parts = (await readdir(work)).sort().flatMap((part) => ['-f', join(work, part)]);
97
+ const text = await executed(installed(pinned.engine), ['-t', String(await cpus()), '-np', '-m', installed(MODEL), ...parts], signal);
98
+ return text
99
+ .split('\n')
100
+ .map((line) => line.trim())
101
+ .filter((line) => line !== '')
102
+ .join(' ');
103
+ }
104
+ finally {
105
+ await rm(work, { recursive: true, force: true });
106
+ }
107
+ }
108
+ export async function transcribe(path, signal) {
109
+ const before = held;
110
+ let release;
111
+ const mine = new Promise((resolve) => {
112
+ release = resolve;
113
+ });
114
+ held = Promise.all([before, mine]);
115
+ try {
116
+ await unlessAborted(before, signal);
117
+ return await transcribed(path, signal);
118
+ }
119
+ finally {
120
+ release();
121
+ }
122
+ }
package/dist/update.js ADDED
@@ -0,0 +1,117 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { run } from './clis.js';
7
+ import { kitHome } from './home.js';
8
+ const RECORD = 'outcome=$1; shift; bash "$0" --linux "$@"; printf %s "$?" > "$outcome"; systemctl start house-kit.service';
9
+ function logged(error) {
10
+ process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
11
+ }
12
+ function directory() {
13
+ return join(kitHome(), 'update');
14
+ }
15
+ function pending() {
16
+ return join(directory(), 'pending.json');
17
+ }
18
+ function outcome() {
19
+ return join(directory(), 'outcome');
20
+ }
21
+ function unit(inputId) {
22
+ return `house-kit-update-${inputId}`;
23
+ }
24
+ async function pendingInput() {
25
+ try {
26
+ return JSON.parse(await readFile(pending(), 'utf8')).input_id;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ async function recorded() {
33
+ const status = await readFile(outcome(), 'utf8').catch(() => null);
34
+ if (status === null)
35
+ return null;
36
+ return status.trim() === '0' ? {} : { refused: `the Kit bootstrap exited with ${status.trim()}` };
37
+ }
38
+ async function finished(inputId) {
39
+ for (;;) {
40
+ const ack = await recorded();
41
+ if (ack !== null)
42
+ return ack;
43
+ if ((await run('systemctl', ['is-active', '--quiet', unit(inputId)], 10_000)).status !== 0) {
44
+ return (await recorded()) ?? { refused: 'the Kit bootstrap did not finish' };
45
+ }
46
+ await new Promise((resolve) => setTimeout(resolve, 1000));
47
+ }
48
+ }
49
+ function rerun(script, inputId, flags) {
50
+ return new Promise((resolve) => {
51
+ const child = spawn('systemd-run', [
52
+ '--wait',
53
+ '--collect',
54
+ '--quiet',
55
+ `--unit=${unit(inputId)}`,
56
+ `--setenv=HOME=${homedir()}`,
57
+ '/bin/sh',
58
+ '-c',
59
+ RECORD,
60
+ script,
61
+ outcome(),
62
+ ...flags,
63
+ ], { stdio: ['ignore', 'ignore', 'inherit'] });
64
+ child.on('error', () => resolve({ refused: 'no service manager runs this Kit' }));
65
+ child.on('close', () => resolve(null));
66
+ });
67
+ }
68
+ export class Updates {
69
+ house;
70
+ held = new Set();
71
+ queue = Promise.resolve();
72
+ constructor(house) {
73
+ this.house = house;
74
+ }
75
+ async resumed() {
76
+ const inputId = await pendingInput();
77
+ if (inputId === null)
78
+ return;
79
+ await this.acknowledge(inputId, await finished(inputId));
80
+ }
81
+ start(input) {
82
+ if (this.held.has(input.input_id))
83
+ return;
84
+ this.held.add(input.input_id);
85
+ this.queue = this.queue
86
+ .then(() => this.carry(input))
87
+ .then((ack) => this.acknowledge(input.input_id, ack))
88
+ .catch(logged)
89
+ .finally(() => this.held.delete(input.input_id));
90
+ }
91
+ async carry(input) {
92
+ if ((await pendingInput()) === input.input_id)
93
+ return finished(input.input_id);
94
+ const answer = await fetch(input.bootstrap);
95
+ if (!answer.ok)
96
+ return { refused: `the Kit bootstrap could not be downloaded: ${answer.status}` };
97
+ const script = Buffer.from(await answer.arrayBuffer());
98
+ if (createHash('sha256').update(script).digest('hex') !== input.sha256) {
99
+ return { refused: 'the Kit bootstrap does not match its sha256' };
100
+ }
101
+ await mkdir(directory(), { recursive: true, mode: 0o700 });
102
+ const path = join(directory(), 'connect-linux.sh');
103
+ await writeFile(path, script, { mode: 0o700 });
104
+ await rm(outcome(), { force: true });
105
+ await writeFile(pending(), `${JSON.stringify({ input_id: input.input_id })}\n`, { mode: 0o600 });
106
+ return (await rerun(path, input.input_id, input.update_clis ? ['--update-clis'] : [])) ?? (await recorded()) ?? {
107
+ refused: 'the Kit bootstrap did not finish',
108
+ };
109
+ }
110
+ async acknowledge(inputId, ack) {
111
+ await this.house.deliver(`/kit/inputs/${inputId}/ack`, ack);
112
+ if ((await pendingInput()) !== inputId)
113
+ return;
114
+ await rm(pending(), { force: true });
115
+ await rm(outcome(), { force: true });
116
+ }
117
+ }
package/how-we-work.md CHANGED
@@ -2,24 +2,22 @@
2
2
 
3
3
  A Room is where information people share is kept; it is not the place for
4
4
  private information. Work goes into the Room it belongs to, or the First Room
5
- when none is named. Read and write Rooms with the `house` CLI.
5
+ when none is named.
6
6
 
7
7
  The skills are already set up; never run `setup-matt-pocock-skills`.
8
8
 
9
9
  ## Where skill output goes
10
10
 
11
- - Glossary (`CONTEXT.md`): `library/glossary.md`
12
- - Decisions (ADRs): `library/decisions/NNNN-<slug>.md`
13
- - Plan (spec): `library/projects/<name>/plan.md`
14
- - Tasks (tickets): `library/projects/<name>/tasks/NN-<slug>.md`, with a
11
+ - Glossary (`CONTEXT.md`): `glossary.md`
12
+ - Decisions (ADRs): `decisions/NNNN-<slug>.md`
13
+ - Plan (spec): `projects/<name>/plan.md`
14
+ - Tasks (tickets): `projects/<name>/tasks/NN-<slug>.md`, with a
15
15
  `Status:` line (`needs-triage`, `needs-info`, `ready-for-agent`,
16
16
  `ready-for-human`, `wontfix`), a `Blocked by:` line and comments under
17
17
  `## Comments`
18
- - Map (Wayfinding): `library/projects/<name>/map.md`, its tasks under
18
+ - Map (Wayfinding): `projects/<name>/map.md`, its tasks under
19
19
  `tasks/` with `Type:`, `Status: claimed|resolved` and `Blocked by:` lines
20
- - Research: `library/research/<slug>.md`
21
- - Questionnaires: `library/questionnaires/<slug>.md`
22
- - Learning: `library/learning/<topic>/`
23
- - Files a person should open, such as HTML pages, are uploaded to the Room and
24
- linked from the document they belong to; a prototype is one HTML file with
25
- every variant on one page.
20
+ - Research: `research/<slug>.md`
21
+ - Questionnaires: `questionnaires/<slug>.md`
22
+ - Learning: `learning/<topic>/`
23
+ - Prototype: one HTML file with every variant on one page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-alpha.9",
3
+ "version": "0.2.1-c5262a8cc063d",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,17 +22,17 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "tsc -p tsconfig.build.json && node scripts/bootstrap-manifest.mjs",
25
+ "engine": "mkdir -p tmp/engine && docker run --rm -v \"$PWD:/kit\" -w /kit alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 sh scripts/engine.sh tmp/engine",
25
26
  "test": "tsc -p tsconfig.json && vitest run"
26
27
  },
27
28
  "dependencies": {
28
29
  "@agentclientprotocol/sdk": "1.7.0",
29
- "@agentshouse/mdmodel": "0.1.0-alpha.17",
30
+ "@agentshouse/mdmodel": "0.1.0-alpha.19",
30
31
  "yaml": "2.9.1"
31
32
  },
32
33
  "peerDependencies": {
33
34
  "@agentclientprotocol/claude-agent-acp": "0.85.1",
34
- "@agentclientprotocol/codex-acp": "2.1.1",
35
- "@xai-official/grok": "1.0.46"
35
+ "@agentclientprotocol/codex-acp": "2.1.1"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@agentclientprotocol/claude-agent-acp": {
@@ -40,9 +40,6 @@
40
40
  },
41
41
  "@agentclientprotocol/codex-acp": {
42
42
  "optional": true
43
- },
44
- "@xai-official/grok": {
45
- "optional": true
46
43
  }
47
44
  },
48
45
  "devDependencies": {