office-core 0.1.0

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 (40) hide show
  1. package/.runtime-dist/scripts/bundle-host-package.js +46 -0
  2. package/.runtime-dist/scripts/demo-multi-agent.js +130 -0
  3. package/.runtime-dist/scripts/home-agent-host.js +1403 -0
  4. package/.runtime-dist/scripts/host-doctor.js +28 -0
  5. package/.runtime-dist/scripts/host-login.js +32 -0
  6. package/.runtime-dist/scripts/host-menu.js +227 -0
  7. package/.runtime-dist/scripts/host-open.js +20 -0
  8. package/.runtime-dist/scripts/install-host.js +108 -0
  9. package/.runtime-dist/scripts/lib/host-config.js +171 -0
  10. package/.runtime-dist/scripts/lib/local-runner.js +287 -0
  11. package/.runtime-dist/scripts/office-cli.js +698 -0
  12. package/.runtime-dist/scripts/run-local-project.js +277 -0
  13. package/.runtime-dist/src/auth/session-token.js +62 -0
  14. package/.runtime-dist/src/discord/outbox-ledger.js +56 -0
  15. package/.runtime-dist/src/do/AgentDO.js +205 -0
  16. package/.runtime-dist/src/do/GatewayShardDO.js +9 -0
  17. package/.runtime-dist/src/do/ProjectDO.js +829 -0
  18. package/.runtime-dist/src/do/TaskDO.js +356 -0
  19. package/.runtime-dist/src/index.js +123 -0
  20. package/.runtime-dist/src/project/office-view.js +405 -0
  21. package/.runtime-dist/src/project/read-model.js +79 -0
  22. package/.runtime-dist/src/routes/agents-bootstrap.js +9 -0
  23. package/.runtime-dist/src/routes/agents-descriptor.js +12 -0
  24. package/.runtime-dist/src/routes/agents-events.js +17 -0
  25. package/.runtime-dist/src/routes/agents-heartbeat.js +21 -0
  26. package/.runtime-dist/src/routes/agents-task-context.js +17 -0
  27. package/.runtime-dist/src/routes/bundles.js +198 -0
  28. package/.runtime-dist/src/routes/local-host.js +49 -0
  29. package/.runtime-dist/src/routes/projects.js +119 -0
  30. package/.runtime-dist/src/routes/tasks.js +67 -0
  31. package/.runtime-dist/src/task/reducer.js +464 -0
  32. package/.runtime-dist/src/types/project.js +1 -0
  33. package/.runtime-dist/src/types/protocol.js +3 -0
  34. package/.runtime-dist/src/types/runtime.js +1 -0
  35. package/README.md +148 -0
  36. package/bin/double-penetration-host.mjs +83 -0
  37. package/package.json +48 -0
  38. package/public/index.html +1581 -0
  39. package/public/install-host.ps1 +64 -0
  40. package/scripts/run-runtime-script.mjs +43 -0
@@ -0,0 +1,464 @@
1
+ import { COMMAND_SCHEMA_VERSION, PROTOCOL_VERSION, } from "../types/protocol.js";
2
+ class ReducerReject extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(code);
6
+ this.code = code;
7
+ }
8
+ }
9
+ const ACCEPTED_BY_PHASE = {
10
+ new: [],
11
+ awareness: [],
12
+ collecting_understandings: ["understanding.submit", "status.upsert", "blocker.raise"],
13
+ alignment: ["understanding.submit", "alignment.proposed", "status.upsert", "blocker.raise"],
14
+ awaiting_human_confirmation: ["status.upsert", "blocker.raise"],
15
+ executing: ["execution.claim", "progress.update", "handoff", "task.complete", "status.upsert", "blocker.raise"],
16
+ blocked_or_handoff: ["execution.claim", "status.upsert", "blocker.raise"],
17
+ completed: ["bundle.ack"],
18
+ };
19
+ export function createTaskState(input) {
20
+ return {
21
+ snapshot: {
22
+ task_id: input.task_id,
23
+ project_id: input.project_id,
24
+ title: input.title,
25
+ phase: input.phase ?? "collecting_understandings",
26
+ task_version: 1,
27
+ event_seq: 0,
28
+ understanding_version: 1,
29
+ manifest_seq: input.manifest_seq ?? 1,
30
+ resolved_mode: "parallel_interpretation",
31
+ confirmation_required: input.confirmation_required ?? false,
32
+ human_confirmed: !(input.confirmation_required ?? false),
33
+ accepted_understanding: null,
34
+ facilitator_agent_id: null,
35
+ current_claim_token: null,
36
+ claimed_by_agent_id: null,
37
+ claim_expires_at: null,
38
+ attempt_count: 0,
39
+ blockers_open: 0,
40
+ updated_at: input.now,
41
+ },
42
+ events: [],
43
+ processed_commands: {},
44
+ project_status_seq: 0,
45
+ };
46
+ }
47
+ export function buildAgentCommandIdempotencyKey(command) {
48
+ return `agent-cmd:${command.agent_id}:${command.session_id}:${command.session_epoch}:${command.command_id}`;
49
+ }
50
+ export function applyTaskCommand(state, command, ctx) {
51
+ const key = buildAgentCommandIdempotencyKey(command);
52
+ const duplicate = state.processed_commands[key];
53
+ if (duplicate) {
54
+ return { state, response: duplicate, side_effects: [] };
55
+ }
56
+ try {
57
+ assertProtocol(command);
58
+ assertFence(command, ctx.current_fence);
59
+ assertObservedVersion(command, state.snapshot.task_version);
60
+ assertBundle(command, state.snapshot.manifest_seq);
61
+ if (command.type === "status.upsert") {
62
+ const payload = command.payload;
63
+ const next = { ...state, project_status_seq: state.project_status_seq + 1 };
64
+ return remember(next, key, accept(command, next.snapshot), [
65
+ {
66
+ kind: "status_upsert",
67
+ data: {
68
+ agent_id: command.agent_id,
69
+ task_id: command.task_id,
70
+ summary: payload.task_summary,
71
+ state: payload.state,
72
+ },
73
+ },
74
+ ]);
75
+ }
76
+ if (command.type === "bundle.ack") {
77
+ return remember(state, key, accept(command, state.snapshot), []);
78
+ }
79
+ if (state.snapshot.confirmation_required && !state.snapshot.human_confirmed && command.type === "execution.claim") {
80
+ throw new ReducerReject("confirmation_required");
81
+ }
82
+ if (requiresLiveClaim(command.type)) {
83
+ assertLiveClaim(state.snapshot, command, ctx.now);
84
+ }
85
+ assertPhase(state.snapshot.phase, command.type);
86
+ if (command.type === "execution.claim" && hasLiveClaim(state.snapshot, ctx.now)) {
87
+ throw new ReducerReject("claim_conflict");
88
+ }
89
+ const event = commandToEvent(state.snapshot, command, ctx);
90
+ let nextSnapshot = rebuildSnapshot(state.snapshot, event);
91
+ if (command.type === "understanding.submit" &&
92
+ state.snapshot.phase === "collecting_understandings" &&
93
+ shouldAdvanceToAlignment(state.events, event)) {
94
+ nextSnapshot = advanceToAlignment(nextSnapshot);
95
+ }
96
+ const nextState = {
97
+ ...state,
98
+ snapshot: nextSnapshot,
99
+ events: [...state.events, event].slice(-500),
100
+ };
101
+ const claimToken = event.event_type === "claim.granted"
102
+ ? event.payload.claim_token
103
+ : undefined;
104
+ return remember(nextState, key, accept(command, nextSnapshot, claimToken), buildSideEffects(state.snapshot, nextSnapshot, event));
105
+ }
106
+ catch (error) {
107
+ if (error instanceof ReducerReject) {
108
+ return remember(state, key, reject(command, state.snapshot, error.code), []);
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ export function applyHumanConfirmation(state, input) {
114
+ const event = input.type === "accept"
115
+ ? {
116
+ id: `evt_${crypto.randomUUID()}`,
117
+ event_type: "human.confirmed",
118
+ actor_type: "human",
119
+ actor_id: input.actor_id,
120
+ payload: {},
121
+ occurred_at: input.now,
122
+ }
123
+ : {
124
+ id: `evt_${crypto.randomUUID()}`,
125
+ event_type: "human.corrected",
126
+ actor_type: "human",
127
+ actor_id: input.actor_id,
128
+ payload: { correction_text: input.correction_text ?? "" },
129
+ occurred_at: input.now,
130
+ };
131
+ return {
132
+ ...state,
133
+ events: [...state.events, event],
134
+ snapshot: rebuildSnapshot(state.snapshot, event),
135
+ };
136
+ }
137
+ export function applyAlarm(state, now) {
138
+ if (!state.snapshot.current_claim_token || !state.snapshot.claim_expires_at) {
139
+ return { state, expired_claim: false };
140
+ }
141
+ if (state.snapshot.claim_expires_at > now) {
142
+ return { state, expired_claim: false };
143
+ }
144
+ const event = {
145
+ id: `evt_${crypto.randomUUID()}`,
146
+ event_type: "claim.expired",
147
+ actor_type: "system",
148
+ actor_id: "TaskDO",
149
+ payload: {
150
+ expired_claim_token: state.snapshot.current_claim_token,
151
+ expired_agent_id: state.snapshot.claimed_by_agent_id,
152
+ },
153
+ occurred_at: now,
154
+ };
155
+ return {
156
+ state: {
157
+ ...state,
158
+ events: [...state.events, event],
159
+ snapshot: rebuildSnapshot(state.snapshot, event),
160
+ },
161
+ expired_claim: true,
162
+ };
163
+ }
164
+ export function scheduleAlarm(snapshot) {
165
+ return snapshot.claim_expires_at;
166
+ }
167
+ function assertProtocol(command) {
168
+ if (command.protocol_version !== PROTOCOL_VERSION) {
169
+ throw new ReducerReject("protocol_mismatch");
170
+ }
171
+ if (command.command_schema_version !== COMMAND_SCHEMA_VERSION) {
172
+ throw new ReducerReject("protocol_mismatch");
173
+ }
174
+ }
175
+ function assertPhase(phase, type) {
176
+ const allowed = ACCEPTED_BY_PHASE[phase] ?? [];
177
+ if (!allowed.includes(type)) {
178
+ throw new ReducerReject("phase_closed");
179
+ }
180
+ }
181
+ function assertFence(command, current) {
182
+ if (command.session_id !== current.session_id || command.session_epoch !== current.session_epoch) {
183
+ throw new ReducerReject("stale_session");
184
+ }
185
+ }
186
+ function assertObservedVersion(command, currentTaskVersion) {
187
+ if (command.observed_task_version < currentTaskVersion) {
188
+ throw new ReducerReject("task_version_conflict");
189
+ }
190
+ }
191
+ function assertBundle(command, manifestSeq) {
192
+ if (manifestSeq !== null && command.bundle_seq < manifestSeq) {
193
+ throw new ReducerReject("stale_bundle");
194
+ }
195
+ }
196
+ function requiresLiveClaim(type) {
197
+ return type === "progress.update" || type === "handoff" || type === "task.complete";
198
+ }
199
+ function hasLiveClaim(snapshot, now) {
200
+ return Boolean(snapshot.current_claim_token && snapshot.claim_expires_at && snapshot.claim_expires_at > now);
201
+ }
202
+ function assertLiveClaim(snapshot, command, now) {
203
+ if (!snapshot.current_claim_token) {
204
+ throw new ReducerReject("claim_conflict");
205
+ }
206
+ if (command.claim_token !== snapshot.current_claim_token) {
207
+ throw new ReducerReject("token_mismatch");
208
+ }
209
+ if (!snapshot.claim_expires_at || snapshot.claim_expires_at <= now) {
210
+ throw new ReducerReject("token_expired");
211
+ }
212
+ }
213
+ function commandToEvent(snapshot, command, ctx) {
214
+ switch (command.type) {
215
+ case "understanding.submit":
216
+ {
217
+ const payload = command.payload;
218
+ return agentEvent(command, "understanding.submitted", {
219
+ text: payload.text,
220
+ confidence: payload.confidence,
221
+ });
222
+ }
223
+ case "alignment.proposed":
224
+ {
225
+ const payload = command.payload;
226
+ return agentEvent(command, "alignment.proposed", {
227
+ proposal_text: payload.proposal_text,
228
+ });
229
+ }
230
+ case "execution.claim":
231
+ {
232
+ const payload = command.payload;
233
+ return agentEvent(command, "claim.granted", {
234
+ claim_token: mintClaimToken(snapshot, command, ctx),
235
+ granted_to_agent_id: command.agent_id,
236
+ expires_at: plusSeconds(ctx.now, ctx.claim_expiry_sec),
237
+ scope: payload.scope,
238
+ approach_summary: payload.approach_summary,
239
+ });
240
+ }
241
+ case "progress.update":
242
+ {
243
+ const payload = command.payload;
244
+ return agentEvent(command, "progress.reported", {
245
+ summary: payload.summary,
246
+ percent_complete: payload.percent_complete,
247
+ });
248
+ }
249
+ case "blocker.raise":
250
+ {
251
+ const payload = command.payload;
252
+ return agentEvent(command, "blocker.raised", {
253
+ message: payload.message,
254
+ new_approach: payload.new_approach ?? false,
255
+ });
256
+ }
257
+ case "handoff":
258
+ {
259
+ const payload = command.payload;
260
+ return agentEvent(command, "handoff.accepted", {
261
+ to_agent_id: payload.to_agent_id ?? null,
262
+ reason: payload.reason,
263
+ context_notes: payload.context_notes,
264
+ });
265
+ }
266
+ case "task.complete":
267
+ {
268
+ const payload = command.payload;
269
+ return agentEvent(command, "task.completed", {
270
+ summary: payload.summary,
271
+ artifact_ids: payload.artifact_ids ?? [],
272
+ });
273
+ }
274
+ default:
275
+ throw new ReducerReject("phase_closed");
276
+ }
277
+ }
278
+ function agentEvent(command, eventType, payload) {
279
+ return {
280
+ id: `evt_${crypto.randomUUID()}`,
281
+ event_type: eventType,
282
+ actor_type: "agent",
283
+ actor_id: command.agent_id,
284
+ payload,
285
+ occurred_at: command.sent_at,
286
+ };
287
+ }
288
+ function mintClaimToken(snapshot, command, ctx) {
289
+ return {
290
+ claim_token: `ctok_${crypto.randomUUID()}`,
291
+ task_id: snapshot.task_id,
292
+ task_version: snapshot.task_version,
293
+ granted_at: ctx.now,
294
+ expires_at: plusSeconds(ctx.now, ctx.claim_expiry_sec),
295
+ granted_to_agent_id: command.agent_id,
296
+ };
297
+ }
298
+ function rebuildSnapshot(previous, event) {
299
+ const next = { ...previous };
300
+ next.event_seq = previous.event_seq + 1;
301
+ next.updated_at = event.occurred_at;
302
+ switch (event.event_type) {
303
+ case "alignment.proposed":
304
+ next.accepted_understanding = String(event.payload.proposal_text ?? "");
305
+ if (next.confirmation_required) {
306
+ next.phase = "awaiting_human_confirmation";
307
+ }
308
+ else {
309
+ next.phase = "executing";
310
+ next.human_confirmed = true;
311
+ next.understanding_version = previous.understanding_version + 1;
312
+ }
313
+ break;
314
+ case "human.confirmed":
315
+ next.phase = "executing";
316
+ next.human_confirmed = true;
317
+ next.understanding_version = previous.understanding_version + 1;
318
+ break;
319
+ case "human.corrected":
320
+ next.phase = "alignment";
321
+ next.human_confirmed = false;
322
+ next.accepted_understanding = String(event.payload.correction_text ?? "");
323
+ next.understanding_version = previous.understanding_version + 1;
324
+ break;
325
+ case "claim.granted":
326
+ next.phase = "executing";
327
+ next.current_claim_token = String(event.payload.claim_token.claim_token);
328
+ next.claimed_by_agent_id = String(event.payload.granted_to_agent_id);
329
+ next.claim_expires_at = String(event.payload.claim_token.expires_at);
330
+ break;
331
+ case "progress.reported":
332
+ next.phase = "executing";
333
+ break;
334
+ case "blocker.raised":
335
+ next.phase = "blocked_or_handoff";
336
+ next.attempt_count = previous.attempt_count + 1;
337
+ next.blockers_open = previous.blockers_open + 1;
338
+ next.current_claim_token = null;
339
+ next.claimed_by_agent_id = null;
340
+ next.claim_expires_at = null;
341
+ break;
342
+ case "handoff.accepted":
343
+ next.phase = "blocked_or_handoff";
344
+ next.current_claim_token = null;
345
+ next.claimed_by_agent_id = null;
346
+ next.claim_expires_at = null;
347
+ break;
348
+ case "claim.expired":
349
+ next.phase = "blocked_or_handoff";
350
+ break;
351
+ case "task.completed":
352
+ next.phase = "completed";
353
+ next.current_claim_token = null;
354
+ next.claimed_by_agent_id = null;
355
+ next.claim_expires_at = null;
356
+ break;
357
+ case "understanding.submitted":
358
+ break;
359
+ }
360
+ if (canonicalSnapshotChanged(previous, next)) {
361
+ next.task_version = previous.task_version + 1;
362
+ }
363
+ return next;
364
+ }
365
+ function shouldAdvanceToAlignment(existingEvents, nextEvent) {
366
+ const agents = new Set();
367
+ for (const event of existingEvents) {
368
+ if (event.event_type === "understanding.submitted") {
369
+ agents.add(event.actor_id);
370
+ }
371
+ }
372
+ if (nextEvent.event_type === "understanding.submitted") {
373
+ agents.add(nextEvent.actor_id);
374
+ }
375
+ return agents.size >= 2;
376
+ }
377
+ function advanceToAlignment(snapshot) {
378
+ if (snapshot.phase === "alignment") {
379
+ return snapshot;
380
+ }
381
+ const next = { ...snapshot };
382
+ next.phase = "alignment";
383
+ next.task_version = snapshot.task_version + 1;
384
+ return next;
385
+ }
386
+ function canonicalSnapshotChanged(previous, next) {
387
+ return (previous.phase !== next.phase ||
388
+ previous.understanding_version !== next.understanding_version ||
389
+ previous.human_confirmed !== next.human_confirmed ||
390
+ previous.accepted_understanding !== next.accepted_understanding ||
391
+ previous.current_claim_token !== next.current_claim_token ||
392
+ previous.claimed_by_agent_id !== next.claimed_by_agent_id ||
393
+ previous.claim_expires_at !== next.claim_expires_at ||
394
+ previous.attempt_count !== next.attempt_count ||
395
+ previous.blockers_open !== next.blockers_open);
396
+ }
397
+ function buildSideEffects(previous, next, event) {
398
+ const effects = [];
399
+ if (event.event_type === "alignment.proposed" || event.event_type === "human.corrected") {
400
+ effects.push({
401
+ kind: "manifest_rebuild",
402
+ data: {
403
+ reason_code: event.event_type === "alignment.proposed"
404
+ ? "human_confirmed_understanding"
405
+ : "human_corrected_understanding",
406
+ task_id: next.task_id,
407
+ task_version: next.task_version,
408
+ expected_prev_manifest_seq: previous.manifest_seq ?? 0,
409
+ },
410
+ });
411
+ }
412
+ if (event.event_type !== "understanding.submitted") {
413
+ effects.push({
414
+ kind: "discord_mirror",
415
+ data: {
416
+ logical_key: `task:${next.task_id}:${event.event_type}:${event.actor_id}`,
417
+ render_version: next.task_version,
418
+ },
419
+ });
420
+ }
421
+ return effects;
422
+ }
423
+ function plusSeconds(now, seconds) {
424
+ return new Date(Date.parse(now) + seconds * 1000).toISOString();
425
+ }
426
+ function accept(command, snapshot, claimToken) {
427
+ return {
428
+ command_id: command.command_id,
429
+ accepted: true,
430
+ current_task_version: snapshot.task_version,
431
+ current_bundle_seq: snapshot.manifest_seq ?? undefined,
432
+ claim_token: claimToken,
433
+ };
434
+ }
435
+ function reject(command, snapshot, code) {
436
+ return {
437
+ command_id: command.command_id,
438
+ accepted: false,
439
+ current_task_version: snapshot.task_version,
440
+ current_bundle_seq: snapshot.manifest_seq ?? undefined,
441
+ rejection: {
442
+ code,
443
+ message: code,
444
+ current_task_version: snapshot.task_version,
445
+ current_bundle_seq: snapshot.manifest_seq ?? undefined,
446
+ },
447
+ };
448
+ }
449
+ const MAX_PROCESSED_COMMANDS = 500;
450
+ function remember(state, key, response, sideEffects) {
451
+ let processed = { ...state.processed_commands, [key]: response };
452
+ const keys = Object.keys(processed);
453
+ if (keys.length > MAX_PROCESSED_COMMANDS) {
454
+ const excess = keys.length - MAX_PROCESSED_COMMANDS;
455
+ for (let i = 0; i < excess; i++) {
456
+ delete processed[keys[i]];
457
+ }
458
+ }
459
+ return {
460
+ state: { ...state, processed_commands: processed },
461
+ response,
462
+ side_effects: sideEffects,
463
+ };
464
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export const PROTOCOL_VERSION = "1.0";
2
+ export const COMMAND_SCHEMA_VERSION = 1;
3
+ export const EVENT_SCHEMA_VERSION = 1;
@@ -0,0 +1 @@
1
+ export {};
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Office Core
2
+
3
+ Office Core is a Cloudflare-hosted control plane with an installable local CLI for Codex and Claude agents.
4
+
5
+ The product model is simple:
6
+
7
+ - one browser UI
8
+ - one active room per project
9
+ - one or more connected machines
10
+ - fresh Codex or Claude agents spawned into the current room
11
+ - room context compacted into `summary.txt`
12
+ - agent startup grounded only on `onboarding.txt` and the current room summary
13
+
14
+ This repo contains:
15
+
16
+ - the Worker and browser UI
17
+ - the local host runtime
18
+ - the local install and host menu scripts
19
+ - the room, host, and task state model
20
+
21
+ ## Project shape
22
+
23
+ - `public/`
24
+ - chat-first browser UI
25
+ - install script served by the Worker
26
+ - `src/`
27
+ - Worker routes
28
+ - Durable Objects
29
+ - project read-models
30
+ - `scripts/`
31
+ - local host runtime
32
+ - install flow
33
+ - host menu and helper scripts
34
+ - `bin/`
35
+ - CLI entrypoint
36
+ - `test/`
37
+ - node test suite
38
+
39
+ ## Requirements
40
+
41
+ - Node.js 20+
42
+ - npm
43
+ - Cloudflare Wrangler for local Worker development
44
+ - Windows if you want the visible local shell flow exactly as designed
45
+
46
+ Optional local runners:
47
+
48
+ - `codex`
49
+ - `claude`
50
+
51
+ ## Install the CLI
52
+
53
+ Install from a packaged tarball served by your Worker:
54
+
55
+ ```powershell
56
+ npm install -g "https://your-worker.example/downloads/office-core.tgz"
57
+ ```
58
+
59
+ Then launch the interactive CLI:
60
+
61
+ ```powershell
62
+ office-core
63
+ ```
64
+
65
+ Primary command:
66
+
67
+ ```powershell
68
+ office-core
69
+ ```
70
+
71
+ Supported aliases:
72
+
73
+ ```powershell
74
+ office-host
75
+ dp-host
76
+ double-penetration-host
77
+ ```
78
+
79
+ If you later publish to npm, the install becomes:
80
+
81
+ ```powershell
82
+ npm install -g office-core
83
+ ```
84
+
85
+ or:
86
+
87
+ ```powershell
88
+ npx office-core
89
+ ```
90
+
91
+ ## Install for local development
92
+
93
+ ```powershell
94
+ cd "C:\Users\Michael Goat Penis\Dev\Office-Core"
95
+ npm install
96
+ ```
97
+
98
+ ## Run locally
99
+
100
+ Window 1:
101
+
102
+ ```powershell
103
+ cd "C:\Users\Michael Goat Penis\Dev\Office-Core"
104
+ npm run dev
105
+ ```
106
+
107
+ Window 2:
108
+
109
+ ```powershell
110
+ cd "C:\Users\Michael Goat Penis\Dev\Office-Core"
111
+ node .\bin\double-penetration-host.mjs install --baseUrl "http://127.0.0.1:8787" --project "prj_local" --workdir "C:\Users\Michael Goat Penis\Dev\Office-Core"
112
+ node .\bin\double-penetration-host.mjs start
113
+ ```
114
+
115
+ Open:
116
+
117
+ - [http://127.0.0.1:8787/?projectId=prj_local](http://127.0.0.1:8787/?projectId=prj_local)
118
+
119
+ ## Useful commands
120
+
121
+ ```powershell
122
+ npm run check
123
+ npm run check:runtime
124
+ npm test
125
+ ```
126
+
127
+ ```powershell
128
+ node .\bin\double-penetration-host.mjs help
129
+ node .\bin\double-penetration-host.mjs doctor
130
+ node .\bin\double-penetration-host.mjs start
131
+ ```
132
+
133
+ ## Current product behavior
134
+
135
+ - conversations are room-first
136
+ - old conversations keep compacted context, not sleeping agents
137
+ - `Add Agent` spawns a fresh visible local shell for the current room
138
+ - room messages are delivered to session inbox/outbox paths
139
+ - agent/system replies are posted back into the room
140
+ - agent recursion is guarded so agents do not endlessly answer each other
141
+
142
+ ## Packaging
143
+
144
+ - `npm pack` produces a prebuilt installable package
145
+ - the tarball is copied to `public/downloads/office-core.tgz`
146
+ - published installs can use `npm install -g office-core`
147
+ - the package includes the compiled `.runtime-dist` runtime, so installed machines do not need TypeScript just to run the CLI
148
+ - repo development can still use `node .\bin\double-penetration-host.mjs` without publishing or linking
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { readFileSync } from "node:fs";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const runtimeRunner = path.join(rootDir, "scripts", "run-runtime-script.mjs");
11
+
12
+ const argv = process.argv.slice(2);
13
+ const [command, ...rest] = argv;
14
+
15
+ if (!command) {
16
+ runScript("scripts/office-cli.ts", rest);
17
+ } else if (command === "menu") {
18
+ runScript("scripts/host-menu.ts", rest);
19
+ } else if (command === "install" || command === "join") {
20
+ runScript("scripts/install-host.ts", rest);
21
+ } else if (command === "start") {
22
+ runScript("scripts/home-agent-host.ts", rest);
23
+ } else if (command === "doctor") {
24
+ runScript("scripts/host-doctor.ts", rest);
25
+ } else if (command === "open") {
26
+ runScript("scripts/host-open.ts", rest);
27
+ } else if (command === "login") {
28
+ runScript("scripts/host-login.ts", rest);
29
+ } else if (command === "help" || command === "--help" || command === "-h") {
30
+ printHelp();
31
+ process.exit(0);
32
+ } else if (command === "version" || command === "--version" || command === "-v") {
33
+ const pkg = JSON.parse(readFileSync(path.join(rootDir, "package.json"), "utf8"));
34
+ console.log(pkg.version);
35
+ process.exit(0);
36
+ } else {
37
+ console.error(`Unknown command: ${command}`);
38
+ printHelp();
39
+ process.exit(1);
40
+ }
41
+
42
+ function runScript(relativeScript, args) {
43
+ const result = spawnSync(process.execPath, [runtimeRunner, relativeScript, ...args], {
44
+ cwd: rootDir,
45
+ stdio: "inherit",
46
+ env: process.env,
47
+ });
48
+
49
+ if (typeof result.status === "number") {
50
+ process.exit(result.status);
51
+ }
52
+
53
+ process.exit(1);
54
+ }
55
+
56
+ function printHelp() {
57
+ console.log(
58
+ [
59
+ "office-core",
60
+ "Aliases: office-host, dp-host, double-penetration-host",
61
+ "",
62
+ "Usage:",
63
+ " office-core Interactive CLI (default)",
64
+ " office-core menu Legacy numbered menu",
65
+ " office-core install Register this machine and save host config",
66
+ " office-core start Start the configured local host daemon",
67
+ " office-core doctor Check worker health, config, and local runners",
68
+ " office-core open Open the configured Office Core UI",
69
+ " office-core login codex Run Codex login locally",
70
+ " office-core login claude Run Claude auth locally",
71
+ "",
72
+ "Legacy aliases continue to work:",
73
+ " office-host install --baseUrl https://your-worker.example --project prj_local --workdir C:\\repo",
74
+ " dp-host install --baseUrl https://your-worker.example --project prj_local --workdir C:\\repo",
75
+ " double-penetration-host start",
76
+ "",
77
+ "Examples:",
78
+ " office-core install --baseUrl https://your-worker.example --project prj_local --workdir C:\\repo",
79
+ " office-core start",
80
+ " office-core doctor",
81
+ ].join("\n"),
82
+ );
83
+ }