@agentshouse/kit 0.2.1-c903d1429e34a → 0.2.1-c96543a4c8b15

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 CHANGED
@@ -38,11 +38,11 @@ reconnects an Environment you already have; `--rooms <ref>[,<ref>...]` gives it
38
38
  only those Rooms and `--no-private` every Room but your Private Room;
39
39
  `--code <code>` connects immediately without a link.
40
40
  A login also connects your own agent: outside an Agent conversation, `house <tool>` calls
41
- House as you and `git` clones and pushes your Apps at `<house>/app/` as you,
42
- until you revoke that connection on Connections. `house help` lists the Tools.
43
- `kit logout` disconnects this computer, stops House Kit and ends your own
44
- agent's connection, so `house` and `git` no longer act as you here; a later
45
- `kit login` reconnects the same Environment.
41
+ House as you and `git` clones and pushes your Apps at `<house>/app/` as you, until you revoke
42
+ that connection on Connections. `house help` lists the Tools. Codex sessions started after a
43
+ login run a lone `house` command outside their sandbox. `kit logout` disconnects this
44
+ computer, stops House Kit and ends your own agent's connection, so `house` and `git` no
45
+ longer act as you here; a later `kit login` reconnects the same Environment.
46
46
 
47
47
  `kit resident` keeps the Environment connected and, as it connects, copies each
48
48
  Room you chose for it as a Git repository in the workspace's `local-copies`.
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c903d1429e34a",
3
+ "version": "0.2.1-c96543a4c8b15",
4
4
  "hosts": {
5
5
  "macos": {
6
6
  "asset": {
7
7
  "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c903d1429e34a/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c96543a4c8b15/connect-linux.sh"
9
9
  }
10
10
  },
11
11
  "linux": {
12
12
  "asset": {
13
13
  "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c903d1429e34a/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c96543a4c8b15/connect-linux.sh"
15
15
  }
16
16
  },
17
17
  "windows": {
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c903d1429e34a/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c96543a4c8b15/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/clis.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { constants, readFileSync } from 'node:fs';
3
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
4
  import { homedir, userInfo } from 'node:os';
5
5
  import { delimiter, isAbsolute, join } from 'node:path';
6
6
  import { kitHome } from './home.js';
7
+ const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
8
+ function houseRule(houses) {
9
+ return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
10
+ }
7
11
  const RAW_SUBTYPES = [
8
12
  'background_tasks_changed',
9
13
  'task_started',
@@ -22,15 +26,37 @@ async function executable(path) {
22
26
  return false;
23
27
  }
24
28
  }
25
- async function allowHouseInCodex(path) {
29
+ async function listedHouses(file) {
30
+ let text;
31
+ try {
32
+ text = await readFile(file, 'utf8');
33
+ }
34
+ catch (error) {
35
+ if (error.code === 'ENOENT')
36
+ return [];
37
+ throw error;
38
+ }
39
+ try {
40
+ const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
41
+ return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ export async function allowHouseInCodex(path) {
26
48
  const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
27
- const houses = [];
28
- for (const candidate of candidates)
29
- if (await executable(candidate))
30
- houses.push(candidate);
31
49
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
50
+ const file = join(rules, 'house.rules');
51
+ const houses = [];
52
+ for (const house of [...(await listedHouses(file)), ...candidates]) {
53
+ if (!houses.includes(house) && (await executable(house)))
54
+ houses.push(house);
55
+ }
32
56
  await mkdir(rules, { recursive: true });
33
- await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
57
+ const temporary = `${file}.${process.pid}`;
58
+ await writeFile(temporary, houseRule(houses));
59
+ await rename(temporary, file);
34
60
  }
35
61
  async function nothing() { }
36
62
  function bridgeInClaudeSandbox(bridge) {
@@ -502,7 +502,7 @@ export class Conversations {
502
502
  if (conversation.turn === turn)
503
503
  conversation.turn = null;
504
504
  const body = Promise.allSettled(turn.uploads).then(() => ({
505
- parts: turn.parts.parts,
505
+ parts: turn.parts.saved(),
506
506
  context: turn.parts.context,
507
507
  ...failed,
508
508
  }));
@@ -647,7 +647,7 @@ export class Conversations {
647
647
  turn.parts.plan(event.entries);
648
648
  return;
649
649
  case 'image':
650
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
650
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
651
651
  this.kit.changed();
652
652
  return;
653
653
  }
@@ -667,7 +667,7 @@ export class Conversations {
667
667
  else
668
668
  conversation.turn.parts.start(part);
669
669
  }
670
- async image(conversation, turn, data, mediaType) {
670
+ async image(conversation, turn, reserved, data, mediaType) {
671
671
  const bytes = Buffer.from(data, 'base64');
672
672
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
673
673
  try {
@@ -680,10 +680,10 @@ export class Conversations {
680
680
  const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
681
681
  if (saved.save.status !== 'saved')
682
682
  throw new Error(saved.save.failure ?? saved.save.status);
683
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
683
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
684
684
  }
685
685
  catch (error) {
686
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
686
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
687
687
  }
688
688
  }
689
689
  }
@@ -2,13 +2,13 @@
2
2
  import { createHash } from 'node:crypto';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { request, setGlobalProxyFromEnv } from 'node:http';
5
- import { basename, join } from 'node:path';
5
+ import { extname, join } from 'node:path';
6
6
  import { HOUSE_REF } from './git.js';
7
7
  import { kitHome, readEnrolment, readOwnAgent } from './home.js';
8
8
  import { callHouse, rpc, UNREACHABLE } from './mcp.js';
9
9
  import { refusalOf } from './refusals.js';
10
10
  import { relayed } from './relay.js';
11
- import { parse } from 'yaml';
11
+ import { parse, stringify } from 'yaml';
12
12
  const USAGE = "usage: house <tool> ['<arguments as JSON>']";
13
13
  const PUSH_USAGE = 'usage: house push [commit] [--owner]';
14
14
  const PUSH = "push [commit]: sends the Local copy's committed changes to House";
@@ -21,6 +21,33 @@ const REVOKED = 'this conversation no longer has House access; nothing to do fro
21
21
  const UNCONNECTED = "this computer's own agent has no House connection; run kit login";
22
22
  const DISCONNECTED = "House no longer accepts this computer's own agent; run kit login";
23
23
  const socketPath = process.env.HOUSE_BRIDGE;
24
+ const MEDIA_TYPES = new Map([
25
+ ['avif', 'image/avif'],
26
+ ['csv', 'text/csv'],
27
+ ['docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
28
+ ['gif', 'image/gif'],
29
+ ['heic', 'image/heic'],
30
+ ['html', 'text/html'],
31
+ ['jpeg', 'image/jpeg'],
32
+ ['jpg', 'image/jpeg'],
33
+ ['json', 'application/json'],
34
+ ['m4a', 'audio/mp4'],
35
+ ['md', 'text/markdown'],
36
+ ['mov', 'video/quicktime'],
37
+ ['mp3', 'audio/mpeg'],
38
+ ['mp4', 'video/mp4'],
39
+ ['ogg', 'audio/ogg'],
40
+ ['pdf', 'application/pdf'],
41
+ ['png', 'image/png'],
42
+ ['pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
43
+ ['svg', 'image/svg+xml'],
44
+ ['txt', 'text/plain'],
45
+ ['wav', 'audio/wav'],
46
+ ['webm', 'video/webm'],
47
+ ['webp', 'image/webp'],
48
+ ['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
49
+ ['zip', 'application/zip'],
50
+ ]);
24
51
  function refused(path, status, text) {
25
52
  const refusal = refusalOf(text);
26
53
  if (refusal !== null)
@@ -98,28 +125,43 @@ async function sent(url, bytes, operation) {
98
125
  return text;
99
126
  throw new Error(refused(url, answer.status, text));
100
127
  }
101
- async function upload(path, roomRef, missing) {
102
- if (typeof path !== 'string' || path === '')
103
- throw new Error(missing);
104
- const bytes = await localFile(path);
105
- const declared = parse(await called('upload', {
106
- ...(roomRef === undefined ? {} : { room_ref: roomRef }),
107
- name: basename(path),
128
+ async function upload({ file, ...destination }) {
129
+ if (typeof file !== 'string' || file === '')
130
+ throw new Error('upload needs "file", a local file');
131
+ const bytes = await localFile(file);
132
+ const mediaType = MEDIA_TYPES.get(extname(file).slice(1).toLowerCase());
133
+ const answer = await called('upload', {
134
+ ...destination,
108
135
  bytes: bytes.length,
109
136
  sha256: createHash('sha256').update(bytes).digest('hex'),
110
- }));
137
+ ...(mediaType === undefined ? {} : { media_type: mediaType }),
138
+ });
139
+ const declared = parse(answer);
111
140
  if (declared.upload === undefined)
112
- return declared.attachment;
113
- const saved = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
114
- if (saved.upload_failure !== undefined)
115
- throw new Error(`${path} was not saved: ${saved.upload_failure}; call again later`);
116
- return saved.attachment;
141
+ return answer;
142
+ const written = JSON.parse(await sent(declared.upload.url, bytes, declared.upload.operation));
143
+ return stringify(written, { lineWidth: 0 }).trimEnd();
144
+ }
145
+ function unexpected(argument, usage) {
146
+ return new Error(`unexpected argument ${argument}; ${usage}`);
147
+ }
148
+ function accepted(args, flags, positionals, usage) {
149
+ const given = { flags: new Set(), positionals: [] };
150
+ for (const arg of args) {
151
+ if (flags.includes(arg))
152
+ given.flags.add(arg);
153
+ else if (!arg.startsWith('-') && given.positionals.length < positionals)
154
+ given.positionals.push(arg);
155
+ else
156
+ throw unexpected(arg, usage);
157
+ }
158
+ return given;
117
159
  }
118
160
  async function push(args) {
119
- if (args.includes('--help'))
161
+ const { flags, positionals: [commit = 'HEAD'] } = accepted(args, ['--help', '--owner'], 1, PUSH_USAGE);
162
+ if (flags.has('--help'))
120
163
  return PUSH_USAGE;
121
- const owner = args.includes('--owner');
122
- const commit = args.find((arg) => arg !== '--owner') ?? 'HEAD';
164
+ const owner = flags.has('--owner');
123
165
  if (owner && socketPath !== undefined)
124
166
  throw new Error('--owner is not for Agent conversations; run house push without it');
125
167
  if (!owner && socketPath === undefined)
@@ -130,7 +172,7 @@ async function push(args) {
130
172
  return pushed.text;
131
173
  }
132
174
  async function fetched(args) {
133
- if (args.includes('--help'))
175
+ if (accepted(args, ['--help'], 0, FETCH_USAGE).flags.has('--help'))
134
176
  return FETCH_USAGE;
135
177
  const answer = JSON.parse(await bridged('/git/fetch', {}, undefined, socketPath ?? join(kitHome(), 'owner.sock')));
136
178
  if (answer.refused)
@@ -151,28 +193,23 @@ function argumentsOf(argument) {
151
193
  throw new Error(ARGUMENTS);
152
194
  return parsed;
153
195
  }
154
- async function house([verb, argument]) {
155
- if (verb === undefined || verb === '--help' || verb === 'help') {
196
+ async function house(args) {
197
+ const { flags, positionals: [verb, argument] } = accepted(args, ['--help'], 2, USAGE);
198
+ if (verb === 'help' && argument !== undefined)
199
+ throw unexpected(argument, USAGE);
200
+ if (flags.has('--help') && argument !== undefined)
201
+ throw unexpected('--help', USAGE);
202
+ if (verb === undefined || verb === 'help') {
156
203
  return [USAGE, 'help', PUSH, FETCH, ...(await tools()).map((tool) => (tool.description === undefined ? tool.name : `${tool.name}: ${tool.description}`))].join('\n');
157
204
  }
158
- if (argument === '--help') {
205
+ if (flags.has('--help')) {
159
206
  const tool = (await tools()).find((listed) => listed.name === verb);
160
207
  if (tool === undefined)
161
208
  throw new Error(`${verb} is no Tool; house find_command '{"query":"${verb}"}' finds Commands`);
162
209
  return [...(tool.description === undefined ? [] : [tool.description]), JSON.stringify(tool.inputSchema)].join('\n');
163
210
  }
164
- const args = argumentsOf(argument);
165
- if (verb === 'upload') {
166
- return JSON.stringify({ attachment: await upload(args.path, args.room_ref, 'upload needs "path", a local file') });
167
- }
168
- if (verb === 'append_record' && Array.isArray(args.attachments)) {
169
- const attachments = [];
170
- for (const path of args.attachments) {
171
- attachments.push(await upload(path, args.room_ref, 'each attachments entry is the path of a local file'));
172
- }
173
- args.attachments = attachments;
174
- }
175
- return called(verb, args);
211
+ const parsed = argumentsOf(argument);
212
+ return verb === 'upload' ? upload(parsed) : called(verb, parsed);
176
213
  }
177
214
  async function called(name, args) {
178
215
  const result = await mcp('tools/call', { name, arguments: args });
@@ -182,7 +219,9 @@ async function called(name, args) {
182
219
  return text;
183
220
  }
184
221
  const argv = process.argv.slice(2);
185
- (argv[0] === 'push' ? push(argv.slice(1)) : argv[0] === 'fetch' ? fetched(argv.slice(1)) : house(argv)).then((text) => {
222
+ const named = argv.findIndex((arg) => !arg.startsWith('-'));
223
+ const rest = argv.filter((_arg, index) => index !== named);
224
+ (argv[named] === 'push' ? push(rest) : argv[named] === 'fetch' ? fetched(rest) : house(argv)).then((text) => {
186
225
  process.stdout.write(`${text}\n`);
187
226
  if (argv[0] === 'shell')
188
227
  process.exitCode = Number(/^exit: (\d+)(?:\n|$)/.exec(text)?.[1] ?? 0);
@@ -1,8 +1,8 @@
1
1
  import { HOUSE_REF } from './git.js';
2
2
  import { HOW_WE_WORK } from './skills.js';
3
3
  const SOURCE_LINE = "From House:";
4
- const HOUSE_LINE = 'Work with House through the `house` CLI: run `house --help`.';
5
- const FILE_LINE = 'To give the User a file or a page, upload it with `house upload` and link it; never start a server.';
4
+ const HOUSE_LINE = "Work with House through the `house` CLI: run `house --help`, and `house find_command` for House's guides and formats.";
5
+ const FILE_LINE = 'To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command \'{"query":"app"}\'`. Never start a server.';
6
6
  const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
7
  const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
8
8
  const UNREADABLE = /^(?:\S+: )?(?:path_not_found|room_not_found|operation_denied)\b/;
package/dist/kit-main.js CHANGED
@@ -1,22 +1,64 @@
1
1
  #!/usr/bin/env node
2
2
  import { setGlobalProxyFromEnv } from 'node:http';
3
+ import { parseArgs } from 'node:util';
3
4
  import { answerGitCredential } from './app-git.js';
4
5
  import { login } from './login.js';
5
6
  import { logout } from './logout.js';
6
7
  import { resident } from './resident.js';
8
+ const USAGE = 'usage: kit <login|logout|resident>\n';
9
+ const COMMANDS = new Map([
10
+ [
11
+ 'logout',
12
+ {
13
+ usage: 'usage: kit logout',
14
+ about: "disconnects this computer, stops House Kit and ends your own agent's connection",
15
+ operands: 0,
16
+ run: logout,
17
+ },
18
+ ],
19
+ [
20
+ 'resident',
21
+ {
22
+ usage: 'usage: kit resident',
23
+ about: "keeps this Environment connected; House Kit's service runs it",
24
+ operands: 0,
25
+ run: resident,
26
+ },
27
+ ],
28
+ [
29
+ 'git-credential',
30
+ {
31
+ usage: 'usage: kit git-credential <get|store|erase>',
32
+ about: "answers git with your own agent's House credential; git runs it",
33
+ operands: 1,
34
+ run: ([operation]) => answerGitCredential(operation),
35
+ },
36
+ ],
37
+ ]);
7
38
  const [command, ...argv] = process.argv.slice(2);
8
39
  setGlobalProxyFromEnv();
9
40
  async function run() {
10
41
  if (command === 'login')
11
42
  return login(argv);
12
- if (command === 'logout')
13
- return logout();
14
- if (command === 'resident')
15
- return resident();
16
- if (command === 'git-credential')
17
- return answerGitCredential(argv[0]);
18
- process.stderr.write('usage: kit <login|logout|resident>\n');
19
- process.exitCode = 2;
43
+ if (command === '--help') {
44
+ parseArgs({ args: argv });
45
+ process.stdout.write(USAGE);
46
+ return;
47
+ }
48
+ const chosen = COMMANDS.get(command ?? '');
49
+ if (chosen === undefined) {
50
+ process.stderr.write(USAGE);
51
+ process.exitCode = 2;
52
+ return;
53
+ }
54
+ const { values, positionals } = parseArgs({ args: argv, options: { help: { type: 'boolean', default: false } }, allowPositionals: true });
55
+ if (positionals.length > chosen.operands)
56
+ throw new Error(`unexpected argument ${positionals[chosen.operands]}; ${chosen.usage}`);
57
+ if (values.help) {
58
+ process.stdout.write(`${chosen.usage}\n ${chosen.about}\n`);
59
+ return;
60
+ }
61
+ return chosen.run(positionals);
20
62
  }
21
63
  run().catch((error) => {
22
64
  process.stderr.write(`kit: ${error instanceof Error ? error.message : String(error)}\n`);
package/dist/login.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
3
  import { hostname } from 'node:os';
4
+ import { delimiter, dirname } from 'node:path';
4
5
  import { parseArgs } from 'node:util';
5
6
  import { post, retrying } from './api.js';
6
7
  import { placeAppGit } from './app-git.js';
8
+ import { allowHouseInCodex, loginPath } from './clis.js';
7
9
  import { forgetOwnAgent, readDisconnected, readEnrolment, readOwnAgent, writeEnrolment, writeOwnAgent } from './home.js';
8
10
  import { callHouse, rpc } from './mcp.js';
9
11
  import { selectRelay } from './relay.js';
10
12
  import { startService } from './service.js';
13
+ import { placeSkillSet } from './skills.js';
11
14
  const DEFAULT_HOUSE = 'https://agents.house';
12
15
  const OPENER = process.platform === 'darwin' ? 'open' : 'xdg-open';
13
16
  // Polling every two seconds brings the User's confirmation to the waiting Kit moments after they give it.
@@ -25,7 +28,8 @@ async function accepted(house, credential) {
25
28
  return false;
26
29
  throw new Error(`House answered ${checked.status} when Kit checked this computer's own agent's connection: ${checked.text}`);
27
30
  }
28
- const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms <ref>[,<ref>...] | --no-private] [--code <code>]
31
+ const USAGE = `usage: kit login [--house <origin>] [--environment <id>] [--rooms <ref>[,<ref>...] | --no-private]
32
+ kit login [--house <origin>] --code <code>
29
33
  --house <origin> the House to connect to, ${DEFAULT_HOUSE} unless one is stored
30
34
  --environment <id> reconnect this Environment
31
35
  --rooms <ref>[,<ref>...] give this Environment only these Rooms
@@ -81,8 +85,9 @@ export async function login(argv) {
81
85
  held = ownAgent !== null && ownAgent.house === house && (await accepted(house, ownAgent.credential));
82
86
  const verifier = randomBytes(32).toString('base64url');
83
87
  const started = await retrying(() => post(house, '/kit', {
88
+ label: hostname(),
84
89
  ...(environment === undefined
85
- ? { act: 'new_environment', label: hostname(), ...(replaces === undefined ? {} : { replaces }) }
90
+ ? { act: 'new_environment', ...(replaces === undefined ? {} : { replaces }) }
86
91
  : { act: 'existing_environment', environment_id: environment }),
87
92
  ...rooms,
88
93
  code_challenge: createHash('sha256').update(verifier).digest('base64url'),
@@ -92,6 +97,19 @@ export async function login(argv) {
92
97
  open(started.start_url);
93
98
  exchanged = await retrying(() => post(house, '/kit/token', { login: started.login, code_verifier: verifier }), undefined, () => POLL_INTERVAL_MS);
94
99
  }
100
+ try {
101
+ await placeSkillSet();
102
+ }
103
+ catch (error) {
104
+ process.stderr.write(`kit: the skills could not be placed: ${error.message}\n`);
105
+ }
106
+ try {
107
+ const shell = await loginPath().catch(() => '');
108
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter));
109
+ }
110
+ catch (error) {
111
+ process.stderr.write(`kit: Codex's rule for house could not be written: ${error.message}\n`);
112
+ }
95
113
  await writeEnrolment({
96
114
  house,
97
115
  ...(relay === undefined ? {} : { relay }),
package/dist/parts.js CHANGED
@@ -2,7 +2,10 @@ export class Parts {
2
2
  parts = [];
3
3
  context = null;
4
4
  emit;
5
+ made = [];
6
+ events = 0;
5
7
  writing = null;
8
+ counting = null;
6
9
  planned = null;
7
10
  constructor(emit) {
8
11
  this.emit = emit;
@@ -10,32 +13,34 @@ export class Parts {
10
13
  write(type, id, text) {
11
14
  if (text === '')
12
15
  return;
13
- const last = this.parts.length - 1;
14
- const part = this.parts[last];
15
- if (this.writing?.type === type && this.writing.id === id && part?.type === type) {
16
- part.text += text;
17
- this.emit({ op: 'append', index: last, text });
16
+ if (this.writing?.type === type && this.writing.id === id) {
17
+ this.parts[this.writing.index].text += text;
18
+ this.emit({ op: 'append', index: this.writing.index, text });
18
19
  return;
19
20
  }
20
- this.start({ type, text });
21
- this.writing = { type, id };
21
+ this.writing = { type, id, index: this.start({ type, text }) };
22
22
  }
23
23
  command() {
24
- const last = this.parts.length - 1;
25
- const part = this.parts[last];
26
- if (part?.type !== 'commands') {
27
- this.start({ type: 'commands', count: 1 });
24
+ if (this.counting === null) {
25
+ this.counting = this.start({ type: 'commands', count: 1 });
28
26
  return;
29
27
  }
28
+ const part = this.parts[this.counting];
30
29
  part.count++;
31
- this.emit({ op: 'count', index: last, count: part.count });
30
+ this.emit({ op: 'count', index: this.counting, count: part.count });
32
31
  }
33
32
  start(part) {
34
33
  this.writing = null;
35
- this.parts.push(part);
36
- const index = this.parts.length - 1;
37
- this.emit({ op: 'start', index, part });
38
- return index;
34
+ this.counting = null;
35
+ return this.add(part, ++this.events);
36
+ }
37
+ reserve() {
38
+ this.writing = null;
39
+ this.counting = null;
40
+ return ++this.events;
41
+ }
42
+ arrive(part, reserved) {
43
+ this.add(part, reserved);
39
44
  }
40
45
  done(index, result) {
41
46
  const part = this.parts[index];
@@ -55,10 +60,23 @@ export class Parts {
55
60
  this.context = context;
56
61
  this.emit({ op: 'context', ...context });
57
62
  }
63
+ saved() {
64
+ return this.parts
65
+ .map((part, index) => ({ part, made: this.made[index] }))
66
+ .sort((a, b) => a.made - b.made)
67
+ .map(({ part }) => part);
68
+ }
58
69
  operations() {
59
70
  return [
60
71
  ...this.parts.map((part, index) => ({ op: 'start', index, part })),
61
72
  ...(this.context === null ? [] : [{ op: 'context', ...this.context }]),
62
73
  ];
63
74
  }
75
+ add(part, made) {
76
+ this.parts.push(part);
77
+ this.made.push(made);
78
+ const index = this.parts.length - 1;
79
+ this.emit({ op: 'start', index, part });
80
+ return index;
81
+ }
64
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c903d1429e34a",
3
+ "version": "0.2.1-c96543a4c8b15",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {