@loompad/cli 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.
- package/ARCHITECTURE.md +211 -0
- package/CHANGELOG.md +470 -0
- package/LICENSE +21 -0
- package/README.md +630 -0
- package/dist/adapters/base.d.ts +71 -0
- package/dist/adapters/base.js +187 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/bridges/antigravity.d.ts +53 -0
- package/dist/adapters/bridges/antigravity.js +106 -0
- package/dist/adapters/bridges/antigravity.js.map +1 -0
- package/dist/adapters/bridges/cdp.d.ts +82 -0
- package/dist/adapters/bridges/cdp.js +222 -0
- package/dist/adapters/bridges/cdp.js.map +1 -0
- package/dist/adapters/bridges/gui-chat.d.ts +113 -0
- package/dist/adapters/bridges/gui-chat.js +498 -0
- package/dist/adapters/bridges/gui-chat.js.map +1 -0
- package/dist/adapters/bridges/kiro.d.ts +19 -0
- package/dist/adapters/bridges/kiro.js +22 -0
- package/dist/adapters/bridges/kiro.js.map +1 -0
- package/dist/adapters/bridges/profiles.d.ts +82 -0
- package/dist/adapters/bridges/profiles.js +125 -0
- package/dist/adapters/bridges/profiles.js.map +1 -0
- package/dist/adapters/claude-code.d.ts +26 -0
- package/dist/adapters/claude-code.js +209 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +51 -0
- package/dist/adapters/codex.js +291 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/echo.d.ts +20 -0
- package/dist/adapters/echo.js +79 -0
- package/dist/adapters/echo.js.map +1 -0
- package/dist/adapters/grok.d.ts +61 -0
- package/dist/adapters/grok.js +217 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +33 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +64 -0
- package/dist/adapters/opencode.js +426 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/cli/common.d.ts +11 -0
- package/dist/cli/common.js +40 -0
- package/dist/cli/common.js.map +1 -0
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +181 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +641 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui-model.d.ts +73 -0
- package/dist/cli/tui-model.js +312 -0
- package/dist/cli/tui-model.js.map +1 -0
- package/dist/cli/tui.d.ts +15 -0
- package/dist/cli/tui.js +451 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/ui.d.ts +10 -0
- package/dist/cli/ui.js +135 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/core/ades.d.ts +79 -0
- package/dist/core/ades.js +130 -0
- package/dist/core/ades.js.map +1 -0
- package/dist/core/baton.d.ts +37 -0
- package/dist/core/baton.js +92 -0
- package/dist/core/baton.js.map +1 -0
- package/dist/core/brain-extract.d.ts +111 -0
- package/dist/core/brain-extract.js +248 -0
- package/dist/core/brain-extract.js.map +1 -0
- package/dist/core/brain-index.d.ts +128 -0
- package/dist/core/brain-index.js +338 -0
- package/dist/core/brain-index.js.map +1 -0
- package/dist/core/brain.d.ts +214 -0
- package/dist/core/brain.js +425 -0
- package/dist/core/brain.js.map +1 -0
- package/dist/core/claude-cli.d.ts +12 -0
- package/dist/core/claude-cli.js +47 -0
- package/dist/core/claude-cli.js.map +1 -0
- package/dist/core/distill.d.ts +19 -0
- package/dist/core/distill.js +88 -0
- package/dist/core/distill.js.map +1 -0
- package/dist/core/eventlog.d.ts +43 -0
- package/dist/core/eventlog.js +211 -0
- package/dist/core/eventlog.js.map +1 -0
- package/dist/core/git.d.ts +203 -0
- package/dist/core/git.js +460 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/logbook.d.ts +59 -0
- package/dist/core/logbook.js +118 -0
- package/dist/core/logbook.js.map +1 -0
- package/dist/core/memory.d.ts +34 -0
- package/dist/core/memory.js +124 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/notify.d.ts +11 -0
- package/dist/core/notify.js +30 -0
- package/dist/core/notify.js.map +1 -0
- package/dist/core/projection.d.ts +20 -0
- package/dist/core/projection.js +96 -0
- package/dist/core/projection.js.map +1 -0
- package/dist/core/registry.d.ts +76 -0
- package/dist/core/registry.js +147 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/router.d.ts +42 -0
- package/dist/core/router.js +99 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/routes.d.ts +93 -0
- package/dist/core/routes.js +399 -0
- package/dist/core/routes.js.map +1 -0
- package/dist/core/search.d.ts +63 -0
- package/dist/core/search.js +0 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/setup.d.ts +70 -0
- package/dist/core/setup.js +250 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/suggestions.d.ts +15 -0
- package/dist/core/suggestions.js +38 -0
- package/dist/core/suggestions.js.map +1 -0
- package/dist/core/worktree.d.ts +32 -0
- package/dist/core/worktree.js +83 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/daemon/app-page.d.ts +32 -0
- package/dist/daemon/app-page.js +6363 -0
- package/dist/daemon/app-page.js.map +1 -0
- package/dist/daemon/auth.d.ts +51 -0
- package/dist/daemon/auth.js +135 -0
- package/dist/daemon/auth.js.map +1 -0
- package/dist/daemon/board.d.ts +117 -0
- package/dist/daemon/board.js +202 -0
- package/dist/daemon/board.js.map +1 -0
- package/dist/daemon/brand-icons.d.ts +29 -0
- package/dist/daemon/brand-icons.js +44 -0
- package/dist/daemon/brand-icons.js.map +1 -0
- package/dist/daemon/client.d.ts +127 -0
- package/dist/daemon/client.js +237 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/geist-font.d.ts +10 -0
- package/dist/daemon/geist-font.js +783 -0
- package/dist/daemon/geist-font.js.map +1 -0
- package/dist/daemon/guards.d.ts +18 -0
- package/dist/daemon/guards.js +40 -0
- package/dist/daemon/guards.js.map +1 -0
- package/dist/daemon/linear.d.ts +59 -0
- package/dist/daemon/linear.js +117 -0
- package/dist/daemon/linear.js.map +1 -0
- package/dist/daemon/push.d.ts +22 -0
- package/dist/daemon/push.js +58 -0
- package/dist/daemon/push.js.map +1 -0
- package/dist/daemon/runtime.d.ts +299 -0
- package/dist/daemon/runtime.js +1016 -0
- package/dist/daemon/runtime.js.map +1 -0
- package/dist/daemon/server.d.ts +125 -0
- package/dist/daemon/server.js +1728 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/tasks.d.ts +153 -0
- package/dist/daemon/tasks.js +264 -0
- package/dist/daemon/tasks.js.map +1 -0
- package/dist/daemon/terminals.d.ts +82 -0
- package/dist/daemon/terminals.js +372 -0
- package/dist/daemon/terminals.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.js +17 -0
- package/dist/sdk.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/docs/SETUP.md +265 -0
- package/docs/adapters.md +132 -0
- package/docs/design-system.md +245 -0
- package/docs/img/board-linear.png +0 -0
- package/docs/img/board-projects.png +0 -0
- package/docs/img/brain.png +0 -0
- package/docs/img/settings.png +0 -0
- package/docs/img/workspace.png +0 -0
- package/docs/integration-notes.md +76 -0
- package/docs/proposals/brain.md +262 -0
- package/package.json +84 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loom — shared types.
|
|
3
|
+
*
|
|
4
|
+
* The event log is the source of truth; everything else (agent memory,
|
|
5
|
+
* board views, phone surfaces) is a projection of it.
|
|
6
|
+
*/
|
|
7
|
+
export type EventKind = "message" | "tool_call" | "tool_result" | "file_edit" | "decision" | "handoff" | "suggestion" | "needs_input" | "run_complete" | "agent_join" | "agent_leave" | "role_change" | "route_started" | "route_step" | "route_paused" | "route_resumed" | "route_completed" | "route_failed" | "turn_diff" | "memory_import" | "memory_add" | "memory_update" | "memory_forget" | "status" | "error";
|
|
8
|
+
/**
|
|
9
|
+
* The chat every event belongs to. A project can hold several conversations;
|
|
10
|
+
* they share one brain, one baton and one working tree — only the talking is
|
|
11
|
+
* split. Events written before chats existed carry no id and read as MAIN.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MAIN_CHAT = "main";
|
|
14
|
+
export interface LoomEvent {
|
|
15
|
+
id: number;
|
|
16
|
+
ts: number;
|
|
17
|
+
kind: EventKind;
|
|
18
|
+
/** Author agent id; absent for human/system events. */
|
|
19
|
+
agentId?: string;
|
|
20
|
+
/** Conversation this belongs to. Absent means the main chat. */
|
|
21
|
+
chat?: string;
|
|
22
|
+
payload: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export type NewEvent = {
|
|
25
|
+
kind: EventKind;
|
|
26
|
+
agentId?: string;
|
|
27
|
+
chat?: string;
|
|
28
|
+
payload: Record<string, unknown>;
|
|
29
|
+
ts?: number;
|
|
30
|
+
};
|
|
31
|
+
/** A named conversation inside a project. */
|
|
32
|
+
export interface ChatInfo {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
createdAt: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* An agent's job, in your words. Free text: call an agent "architect",
|
|
39
|
+
* "tester", "the one that writes docs" — whatever your project actually does.
|
|
40
|
+
*
|
|
41
|
+
* A few names carry extra meaning if you use them, and none if you don't:
|
|
42
|
+
* `buildDefaultRoutes` seeds a plan→execute→review pipeline when it sees
|
|
43
|
+
* planner/executor/reviewer, the rules router prefers a reviewer last, and a
|
|
44
|
+
* route step matches an agent by role as well as by id (so a route named
|
|
45
|
+
* ["architect","tester"] just works). Nothing requires them.
|
|
46
|
+
*/
|
|
47
|
+
export type AgentRole = string;
|
|
48
|
+
/** Roles Loom suggests when it sets a project up. Only suggestions. */
|
|
49
|
+
export declare const SUGGESTED_ROLES: readonly ["planner", "executor", "reviewer", "general"];
|
|
50
|
+
/** Adapter = full-duplex; bridge = read-mostly, never holds the baton. */
|
|
51
|
+
export type AgentTier = "adapter" | "bridge";
|
|
52
|
+
export interface AgentConfig {
|
|
53
|
+
/** Stable instance id within the project, e.g. "claude-code". */
|
|
54
|
+
id: string;
|
|
55
|
+
/** Adapter kind, e.g. "claude-code" | "opencode" | "echo" | "antigravity". */
|
|
56
|
+
kind: string;
|
|
57
|
+
role: AgentRole;
|
|
58
|
+
options?: Record<string, unknown>;
|
|
59
|
+
/** Override the ADE's native memory files to import into the shared brain. */
|
|
60
|
+
memoryFiles?: string[];
|
|
61
|
+
}
|
|
62
|
+
/** One ADE's native memory pulled into the unified brain. */
|
|
63
|
+
export interface MemorySource {
|
|
64
|
+
agentId: string;
|
|
65
|
+
kind: string;
|
|
66
|
+
file: string;
|
|
67
|
+
chars: number;
|
|
68
|
+
}
|
|
69
|
+
export interface UnifiedMemory {
|
|
70
|
+
projectName: string;
|
|
71
|
+
decisions: string[];
|
|
72
|
+
sources: MemorySource[];
|
|
73
|
+
/** The merged brain document (decisions + imported ADE memories + context). */
|
|
74
|
+
document: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A route step: which agent (by id or role), and what job it does.
|
|
78
|
+
*
|
|
79
|
+
* `role` is the job for THIS step — planner/executor/reviewer/… — chosen when
|
|
80
|
+
* the task is created, independent of the agent's own default role. So the same
|
|
81
|
+
* agent can plan in one task and review in another. `instruction` is optional
|
|
82
|
+
* free-text focus on top of the role.
|
|
83
|
+
*/
|
|
84
|
+
export type RouteStepSpec = string | {
|
|
85
|
+
step: string;
|
|
86
|
+
role?: string;
|
|
87
|
+
instruction?: string;
|
|
88
|
+
};
|
|
89
|
+
/** How shared memory is rendered on handoff. */
|
|
90
|
+
export interface ProjectionConfig {
|
|
91
|
+
/** "template" (default, free, instant) or "llm" (distilled by a small Claude). */
|
|
92
|
+
mode?: "template" | "llm";
|
|
93
|
+
model?: string;
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
}
|
|
96
|
+
/** The brain's phase-2 extractor — learning from each turn. See core/brain-extract.ts. */
|
|
97
|
+
export interface BrainConfig {
|
|
98
|
+
/**
|
|
99
|
+
* Who reads finished turns for memory:
|
|
100
|
+
* "auto" (default) — a small Claude, when the CLI is available; a no-op when not.
|
|
101
|
+
* "off" — never extract; the brain only holds what you type.
|
|
102
|
+
*/
|
|
103
|
+
extractor?: "auto" | "off";
|
|
104
|
+
/** Model for the extractor call; small+fast is right. */
|
|
105
|
+
model?: string;
|
|
106
|
+
}
|
|
107
|
+
export interface ProjectConfig {
|
|
108
|
+
name: string;
|
|
109
|
+
agents: AgentConfig[];
|
|
110
|
+
/** Agent that receives messages when nobody holds the baton. */
|
|
111
|
+
defaultAgent?: string;
|
|
112
|
+
/** Named multi-hop pipelines; steps are agent ids/roles, optionally with instructions. */
|
|
113
|
+
routes?: Record<string, RouteStepSpec[]>;
|
|
114
|
+
projection?: ProjectionConfig;
|
|
115
|
+
brain?: BrainConfig;
|
|
116
|
+
}
|
|
117
|
+
export type RouteStatus = "running" | "waiting_human" | "completed" | "failed" | "aborted";
|
|
118
|
+
export type RouteMode = "static" | "dynamic";
|
|
119
|
+
export type RouterKind = "rules" | "llm";
|
|
120
|
+
export interface RouteState {
|
|
121
|
+
id: string;
|
|
122
|
+
name?: string;
|
|
123
|
+
task: string;
|
|
124
|
+
/** Resolved agent ids, in order. Dynamic routes grow this per hop. */
|
|
125
|
+
steps: string[];
|
|
126
|
+
stepRoles: AgentRole[];
|
|
127
|
+
/** Optional per-step focus text (parallel to steps; null = role default only). */
|
|
128
|
+
stepInstructions?: Array<string | null>;
|
|
129
|
+
/** Index of the step currently executing (or last executed). */
|
|
130
|
+
current: number;
|
|
131
|
+
status: RouteStatus;
|
|
132
|
+
/** static = fixed pipeline; dynamic = a router picks each next hop. */
|
|
133
|
+
mode: RouteMode;
|
|
134
|
+
router?: RouterKind;
|
|
135
|
+
maxHops?: number;
|
|
136
|
+
startedAt: number;
|
|
137
|
+
updatedAt: number;
|
|
138
|
+
reason?: string;
|
|
139
|
+
pendingQuestion?: string;
|
|
140
|
+
/** Project cost total when the route started (internal baseline). */
|
|
141
|
+
costStartUsd?: number;
|
|
142
|
+
/** Spend attributed to this route (set when it ends). */
|
|
143
|
+
costUsd?: number;
|
|
144
|
+
}
|
|
145
|
+
export interface ProjectInfo {
|
|
146
|
+
id: string;
|
|
147
|
+
name: string;
|
|
148
|
+
dir: string;
|
|
149
|
+
}
|
|
150
|
+
export interface SendInput {
|
|
151
|
+
text: string;
|
|
152
|
+
/** One-shot handoff briefing injected alongside this turn. */
|
|
153
|
+
briefing?: string;
|
|
154
|
+
}
|
|
155
|
+
export interface AdapterEvent {
|
|
156
|
+
kind: EventKind;
|
|
157
|
+
payload: Record<string, unknown>;
|
|
158
|
+
}
|
|
159
|
+
export interface AgentCapabilities {
|
|
160
|
+
tier: AgentTier;
|
|
161
|
+
send: boolean;
|
|
162
|
+
stream: boolean;
|
|
163
|
+
injectMemory: boolean;
|
|
164
|
+
interrupt: boolean;
|
|
165
|
+
diff: boolean;
|
|
166
|
+
}
|
|
167
|
+
export interface BaseAgent {
|
|
168
|
+
readonly id: string;
|
|
169
|
+
readonly kind: string;
|
|
170
|
+
readonly capabilities: AgentCapabilities;
|
|
171
|
+
/** Is the underlying tool installed/reachable? */
|
|
172
|
+
available(): Promise<boolean>;
|
|
173
|
+
start(): Promise<void>;
|
|
174
|
+
stop(): Promise<void>;
|
|
175
|
+
/** Persist Loom's namespaced memory projection for this agent. */
|
|
176
|
+
injectMemory(projection: string): Promise<void>;
|
|
177
|
+
/** Subscribe to live events; returns unsubscribe. */
|
|
178
|
+
onEvent(cb: (e: AdapterEvent) => void): () => void;
|
|
179
|
+
}
|
|
180
|
+
/** Full-duplex adapter — may hold the baton. */
|
|
181
|
+
export interface Adapter extends BaseAgent {
|
|
182
|
+
send(input: SendInput): Promise<void>;
|
|
183
|
+
interrupt(): Promise<void>;
|
|
184
|
+
/** Working-tree changes attributable to the agent (porcelain-ish). */
|
|
185
|
+
diff(): Promise<string>;
|
|
186
|
+
busy(): boolean;
|
|
187
|
+
}
|
|
188
|
+
/** Read-mostly bridge — never holds the baton (GUI agents). */
|
|
189
|
+
export interface Bridge extends BaseAgent {
|
|
190
|
+
}
|
|
191
|
+
export type AnyAgent = Adapter | Bridge;
|
|
192
|
+
export declare function isAdapter(a: AnyAgent): a is Adapter;
|
|
193
|
+
export interface AgentStatus {
|
|
194
|
+
id: string;
|
|
195
|
+
kind: string;
|
|
196
|
+
role: AgentRole;
|
|
197
|
+
tier: AgentTier;
|
|
198
|
+
available: boolean;
|
|
199
|
+
busy: boolean;
|
|
200
|
+
holdsBaton: boolean;
|
|
201
|
+
/** The model override in effect, or "" for the CLI's own default. */
|
|
202
|
+
model: string;
|
|
203
|
+
}
|
|
204
|
+
export interface ProjectStatus {
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
dir: string;
|
|
208
|
+
holder: string | null;
|
|
209
|
+
agents: AgentStatus[];
|
|
210
|
+
lastEvent: LoomEvent | null;
|
|
211
|
+
needsInput: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* The agent whose question is still unanswered, when `needsInput` — the
|
|
214
|
+
* board puts a name on the card, not just "someone is blocked".
|
|
215
|
+
*/
|
|
216
|
+
blockedAgent?: string | null;
|
|
217
|
+
/** Conversations in this project, main first. */
|
|
218
|
+
chats?: ChatInfo[];
|
|
219
|
+
route?: RouteState | null;
|
|
220
|
+
/** Named pipelines defined in config (for pickers/dropdowns). */
|
|
221
|
+
routeNames?: string[];
|
|
222
|
+
/** Lifetime spend across all agents in this project (USD). */
|
|
223
|
+
costUsd?: number;
|
|
224
|
+
}
|
|
225
|
+
export interface AgentCost {
|
|
226
|
+
agentId: string;
|
|
227
|
+
usd: number;
|
|
228
|
+
turns: number;
|
|
229
|
+
ms: number;
|
|
230
|
+
}
|
|
231
|
+
export interface CostSummary {
|
|
232
|
+
totalUsd: number;
|
|
233
|
+
turns: number;
|
|
234
|
+
totalMs: number;
|
|
235
|
+
byAgent: AgentCost[];
|
|
236
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loom — shared types.
|
|
3
|
+
*
|
|
4
|
+
* The event log is the source of truth; everything else (agent memory,
|
|
5
|
+
* board views, phone surfaces) is a projection of it.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The chat every event belongs to. A project can hold several conversations;
|
|
9
|
+
* they share one brain, one baton and one working tree — only the talking is
|
|
10
|
+
* split. Events written before chats existed carry no id and read as MAIN.
|
|
11
|
+
*/
|
|
12
|
+
export const MAIN_CHAT = "main";
|
|
13
|
+
/** Roles Loom suggests when it sets a project up. Only suggestions. */
|
|
14
|
+
export const SUGGESTED_ROLES = ["planner", "executor", "reviewer", "general"];
|
|
15
|
+
export function isAdapter(a) {
|
|
16
|
+
return a.capabilities.tier === "adapter";
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoCH;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AA4ChC,uEAAuE;AACvE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,CAAU,CAAC;AAsKvF,MAAM,UAAU,SAAS,CAAC,CAAW;IACnC,OAAO,CAAC,CAAC,YAAY,CAAC,IAAI,KAAK,SAAS,CAAC;AAC3C,CAAC"}
|
package/docs/SETUP.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Setting up Loom
|
|
2
|
+
|
|
3
|
+
Read this before installing. Loom drives other people's coding agents, so most
|
|
4
|
+
of the setup is really *their* setup — and the parts that bite are the ones
|
|
5
|
+
nobody warns you about.
|
|
6
|
+
|
|
7
|
+
Everything here was hit for real on a machine, not copied from a wiki. Where
|
|
8
|
+
something is a guess, it says so.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. The one hard requirement: Node ≥ 22.5
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
node --version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Loom's event log uses `node:sqlite`, which arrived in Node 22.5. On anything
|
|
19
|
+
older Loom still runs and **silently falls back to a JSONL store with no
|
|
20
|
+
history** — the app looks fine and your past turns aren't there.
|
|
21
|
+
|
|
22
|
+
> **The installed desktop app has this problem too.** Electron 33 bundles Node
|
|
23
|
+
> 20, so `Loom.app` looks for a real `node` on your machine (`$LOOM_NODE`, then
|
|
24
|
+
> the usual install paths, then `PATH`). If it can't find one, it runs on
|
|
25
|
+
> Electron's Node and degrades exactly as above. There is no warning yet. See
|
|
26
|
+
> `desktop/BUILD.md`.
|
|
27
|
+
|
|
28
|
+
- **macOS**: `brew install node`
|
|
29
|
+
- **Windows**: `winget install OpenJS.NodeJS`
|
|
30
|
+
- **Linux**: your distro's node, or [nodesource](https://github.com/nodesource/distributions)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Install at least one agent
|
|
35
|
+
|
|
36
|
+
Loom has nothing to drive on its own. A new project is given whichever of these
|
|
37
|
+
it finds — and **an empty roster if it finds none**, which is honest but not
|
|
38
|
+
useful.
|
|
39
|
+
|
|
40
|
+
| Agent | Install | Sign in | Check |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| **Claude Code** | `npm i -g @anthropic-ai/claude-code` | `claude` (once, interactively) | `claude --version` |
|
|
43
|
+
| **Codex** | [Codex.app](https://openai.com/codex/), or `npm i -g @openai/codex` | `codex login` | `codex login status` |
|
|
44
|
+
| **OpenCode** | `curl -fsSL https://opencode.ai/install \| bash` | `opencode auth login` | `opencode --version` |
|
|
45
|
+
| **Grok Code** | [docs.x.ai](https://docs.x.ai) | `grok` (once, interactively) | `grok --version` |
|
|
46
|
+
|
|
47
|
+
Two things worth knowing:
|
|
48
|
+
|
|
49
|
+
**Codex's CLI hides inside its app.** On a Mac with Codex.app installed and
|
|
50
|
+
nothing on `PATH`, the binary is at
|
|
51
|
+
`/Applications/Codex.app/Contents/Resources/codex`. Loom looks there as well as
|
|
52
|
+
on `PATH`, so you don't have to do anything — but that's why `which codex`
|
|
53
|
+
coming up empty doesn't mean it's missing.
|
|
54
|
+
|
|
55
|
+
**Being logged in is not optional and not detectable in advance.** Loom probes
|
|
56
|
+
whether a CLI *exists*, not whether it's authenticated. An unauthenticated agent
|
|
57
|
+
looks installed, takes your turn, and fails. Run each one once by hand first.
|
|
58
|
+
|
|
59
|
+
Then ask Loom what it can see:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
loom doctor
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. GUI agents: Antigravity IDE and Kiro
|
|
68
|
+
|
|
69
|
+
These have no API and no headless mode. Loom drives them through the Chrome
|
|
70
|
+
DevTools port they open *only if you ask*, so they need a launch flag.
|
|
71
|
+
|
|
72
|
+
### Which Antigravity?
|
|
73
|
+
|
|
74
|
+
There are two apps and only one of them is the right one.
|
|
75
|
+
|
|
76
|
+
| | What it is | Use it? |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **Antigravity.app** | the Manager — a web page behind a Google sign-in | ❌ no chat DOM at all |
|
|
79
|
+
| **Antigravity IDE.app** | the VS Code fork with the agent in it | ✅ this one |
|
|
80
|
+
|
|
81
|
+
Pointing Loom at the Manager finds nothing and reports that Antigravity has no
|
|
82
|
+
chat. It's the wrong app, not a broken bridge.
|
|
83
|
+
|
|
84
|
+
### Why port 9333 and not 9222
|
|
85
|
+
|
|
86
|
+
Antigravity's own Browser Control (the Chrome button in its toolbar) already
|
|
87
|
+
uses **9222**. Ask for it and you get `EADDRINUSE` — and the IDE starts anyway,
|
|
88
|
+
just without a debugger, silently. Use **9333**.
|
|
89
|
+
(Discovered by the [AntiGravity-AutoAccept](https://github.com/yazanbaker94/AntiGravity-AutoAccept)
|
|
90
|
+
project.)
|
|
91
|
+
|
|
92
|
+
### macOS
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
open -a "Antigravity IDE" --args --remote-debugging-port=9333 /path/to/your/repo
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Make it permanent in `~/.zshrc`:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
alias antigravity='open -a "Antigravity IDE" --args --remote-debugging-port=9333'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If `open -a` doesn't pass the flag through on your build, go direct:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
alias antigravity='"/Applications/Antigravity IDE.app/Contents/MacOS/Electron" --remote-debugging-port=9333 & disown'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or make an Automator **Application** with one *Run Shell Script* action
|
|
111
|
+
containing the `open -a` line, and launch that instead of the dock icon.
|
|
112
|
+
|
|
113
|
+
**Only one instance gets the port.** If Antigravity is already running, a second
|
|
114
|
+
launch just focuses the first window and your flag is ignored — the port stays
|
|
115
|
+
closed and nothing says so. Quit it fully first:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
pkill -f "Antigravity" && sleep 2
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Windows
|
|
122
|
+
|
|
123
|
+
Right-click the Antigravity IDE shortcut → **Properties** → append to *Target*:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
--remote-debugging-port=9333
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Launch from that shortcut afterwards, not from search results or a pinned
|
|
130
|
+
taskbar entry pointing elsewhere.
|
|
131
|
+
|
|
132
|
+
### Linux
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
# find the launcher
|
|
136
|
+
find /usr/share/applications ~/.local/share/applications -name "*ntigravity*"
|
|
137
|
+
# then edit its Exec line:
|
|
138
|
+
Exec=/path/to/antigravity --remote-debugging-port=9333 %F
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
or an alias in `~/.bashrc`:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
alias antigravity='antigravity --remote-debugging-port=9333'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Kiro
|
|
148
|
+
|
|
149
|
+
Same idea, **port 9334** so it can't collide with Antigravity:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
open -a "Kiro" --args --remote-debugging-port=9334 # macOS
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Kiro's chat panel must be **open** — on a fresh window its only editable area is
|
|
156
|
+
Monaco, holding your source file, and Loom refuses to type into that on purpose.
|
|
157
|
+
|
|
158
|
+
### Then check it
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
loom doctor
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Loom distinguishes *reachable* from *driveable*, because a signed-out app
|
|
165
|
+
answers the debugger cheerfully and has no usable chat:
|
|
166
|
+
|
|
167
|
+
- `Antigravity IDE isn't listening — open -a "Antigravity IDE" --args --remote-debugging-port=9333` → the flag didn't take
|
|
168
|
+
- `Antigravity IDE is signed out — log in from its window` → the app is up; **log into it**
|
|
169
|
+
- `no chat panel — open a chat` → it's up and signed in; open a conversation
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 4. Permissions
|
|
174
|
+
|
|
175
|
+
### macOS
|
|
176
|
+
|
|
177
|
+
| What | Needed for | How |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| **Notifications** | "your agent needs you" while you're elsewhere | First `loom up` asks. Otherwise: System Settings → Notifications → Loom |
|
|
180
|
+
| **Open a downloaded app** | Loom.app isn't notarized yet | **Right-click → Open** the first time. Double-clicking gives "cannot be opened" with no way forward |
|
|
181
|
+
| **Local Network** | your phone reaching the daemon over LAN | Prompted on first connection. System Settings → Privacy & Security → Local Network |
|
|
182
|
+
|
|
183
|
+
**Not needed, despite what you might expect:** Accessibility, Screen Recording,
|
|
184
|
+
Automation, Full Disk Access. Loom drives GUI agents through their debugging
|
|
185
|
+
port, not by pretending to be a mouse, so it never asks the OS for control of
|
|
186
|
+
another app. If something tells you to grant Accessibility to Loom, that isn't
|
|
187
|
+
Loom.
|
|
188
|
+
|
|
189
|
+
### Windows
|
|
190
|
+
|
|
191
|
+
| What | Needed for | How |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| **Firewall (private networks)** | your phone reaching the daemon | Windows Defender prompts on first `loom up` — tick **Private networks**. Public: leave off |
|
|
194
|
+
| **SmartScreen** | the installer isn't signed yet | **More info → Run anyway** |
|
|
195
|
+
| **Notifications** | agent alerts | Settings → System → Notifications → Loom |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 5. Staying open, and staying running
|
|
200
|
+
|
|
201
|
+
**The daemon already outlives the window.** It's spawned detached — close the
|
|
202
|
+
app and your agents keep working; reopen and you're back where you were. That's
|
|
203
|
+
the design, not a leak. To stop it for real:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
loom down
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Run it at login (optional).** No installer wires this up yet; do it yourself:
|
|
210
|
+
|
|
211
|
+
- **macOS** — System Settings → General → Login Items → **+** → Loom.app. Or a
|
|
212
|
+
LaunchAgent running `loom up`.
|
|
213
|
+
- **Windows** — `Win+R` → `shell:startup` → shortcut to Loom.
|
|
214
|
+
- **Linux** — a user systemd unit running `loom up`.
|
|
215
|
+
|
|
216
|
+
**Always on top** is the window manager's job, not Loom's: right-click the title
|
|
217
|
+
bar (Windows), or use a tiling/stage manager (macOS). Loom doesn't ask for it —
|
|
218
|
+
a tool that forces itself in front of your editor is a tool you'll uninstall.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 6. Your phone
|
|
223
|
+
|
|
224
|
+
The phone app talks to the daemon on your own machine. Nothing goes through a
|
|
225
|
+
server of ours, because there isn't one.
|
|
226
|
+
|
|
227
|
+
1. **Install Tailscale** on both, sign into the same tailnet: [tailscale.com/download](https://tailscale.com/download)
|
|
228
|
+
2. **Bind Loom to the tailnet** — by default it listens on localhost only and your phone cannot see it:
|
|
229
|
+
```sh
|
|
230
|
+
loom up --restart --tailnet
|
|
231
|
+
```
|
|
232
|
+
3. **Pair**: `loom pair` prints a QR code. Scan it.
|
|
233
|
+
|
|
234
|
+
Two things that will confuse you otherwise:
|
|
235
|
+
|
|
236
|
+
- `loom doctor` saying *"binding localhost only — phones can't reach it"* is the
|
|
237
|
+
whole answer. Re-run with `--tailnet`.
|
|
238
|
+
- The pairing token is single-use and the client token it hands back lives on the
|
|
239
|
+
phone. Losing the phone means `loom` → unpair that client, not rotating
|
|
240
|
+
everything.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 7. When it's wrong
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
loom doctor # what Loom can see, and what it can't
|
|
248
|
+
tail -f ~/.loom/daemon.log
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**A change you made isn't showing up.** The daemon is a long-lived process; it
|
|
252
|
+
serves the code it started with. `loom up` compares fingerprints and restarts a
|
|
253
|
+
stale one automatically, but if you're staring at yesterday's behaviour:
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
loom down && loom up
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**"daemon already running" but it's on the wrong port.** Something else has
|
|
260
|
+
7420. `lsof -ti tcp:7420` (macOS/Linux) or `netstat -ano | findstr 7420`
|
|
261
|
+
(Windows).
|
|
262
|
+
|
|
263
|
+
**An agent takes the turn and nothing happens.** It's almost always
|
|
264
|
+
authentication. Run that agent by hand in a terminal once — it'll tell you what
|
|
265
|
+
Loom can't.
|
package/docs/adapters.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Writing a Loom adapter or bridge
|
|
2
|
+
|
|
3
|
+
Loom's integration model has **two tiers**, and picking the right one is the first
|
|
4
|
+
decision:
|
|
5
|
+
|
|
6
|
+
| | Adapter | Bridge |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| Can hold the baton (write lock) | ✅ | ❌ never |
|
|
9
|
+
| Must implement | `send`, live events, `injectMemory`, `interrupt`, `diff` | events (best-effort), `injectMemory` |
|
|
10
|
+
| For | agents with a real API / headless mode | GUI agents without a stable control surface |
|
|
11
|
+
| Examples | Claude Code, OpenCode | Antigravity |
|
|
12
|
+
|
|
13
|
+
This split is deliberate (see [ARCHITECTURE.md](../ARCHITECTURE.md)): the baton
|
|
14
|
+
authorizes *edits to the user's working tree*, so it can only be held by an agent Loom
|
|
15
|
+
can reliably start, stream, interrupt, and account for. If your target tool only exposes
|
|
16
|
+
a debug port and pixels, build a bridge; if it exposes an API, build an adapter. A
|
|
17
|
+
bridge can graduate later.
|
|
18
|
+
|
|
19
|
+
## The contract
|
|
20
|
+
|
|
21
|
+
Everything lives in `threadloom/sdk`:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
AdapterBase, // extend for adapters
|
|
26
|
+
BridgeBase, // extend for bridges
|
|
27
|
+
registerAgentKind,
|
|
28
|
+
type SendInput, // { text, briefing? }
|
|
29
|
+
type AdapterEvent // { kind, payload }
|
|
30
|
+
} from "threadloom/sdk";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Events you can emit
|
|
34
|
+
|
|
35
|
+
| kind | payload | when |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `message` | `{ text }` | your agent produced prose |
|
|
38
|
+
| `tool_call` | `{ tool, summary }` | it used a tool |
|
|
39
|
+
| `file_edit` | `{ path }` | it changed a file |
|
|
40
|
+
| `needs_input` | `{ question }` | it is blocked on the human → triggers a notification |
|
|
41
|
+
| `run_complete` | `{ durationMs?, costUsd? }` | the turn finished → notification |
|
|
42
|
+
| `status` | `{ state, … }` | lifecycle (ready / interrupted / …) |
|
|
43
|
+
| `error` | `{ message }` | something broke |
|
|
44
|
+
|
|
45
|
+
Every event you emit is appended to the project's event log — which is exactly what gets
|
|
46
|
+
projected into the *next* agent's memory on handoff. Emit honestly and your agent's work
|
|
47
|
+
becomes context for everyone else.
|
|
48
|
+
|
|
49
|
+
### A complete adapter
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { AdapterBase, registerAgentKind, type SendInput } from "threadloom/sdk";
|
|
53
|
+
|
|
54
|
+
export class MyAgentAdapter extends AdapterBase {
|
|
55
|
+
// available(): is the underlying tool installed/reachable?
|
|
56
|
+
async available(): Promise<boolean> {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async start(): Promise<void> {
|
|
61
|
+
// boot your server / warm your session; emit a status event
|
|
62
|
+
this.emit({ kind: "status", payload: { state: "ready" } });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async stop(): Promise<void> {}
|
|
66
|
+
|
|
67
|
+
async send(input: SendInput): Promise<void> {
|
|
68
|
+
if (this._busy) throw new Error(`${this.id} is busy`);
|
|
69
|
+
this._busy = true;
|
|
70
|
+
try {
|
|
71
|
+
// input.briefing is the one-shot handoff context — inject it however
|
|
72
|
+
// your agent accepts system/context input. Claude Code uses
|
|
73
|
+
// --append-system-prompt; OpenCode gets a delimited preamble.
|
|
74
|
+
const reply = await callMyAgent(input.text, input.briefing);
|
|
75
|
+
this.emit({ kind: "message", payload: { text: reply } });
|
|
76
|
+
this.emit({ kind: "run_complete", payload: {} });
|
|
77
|
+
} finally {
|
|
78
|
+
this._busy = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async interrupt(): Promise<void> {
|
|
83
|
+
// stop the in-flight turn (kill the process, call the abort route, …)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// diff() has a default implementation (git status --porcelain).
|
|
87
|
+
// injectMemory() has a default implementation writing the namespaced
|
|
88
|
+
// .loom/memory/<id>.md file — override only if your agent has a richer
|
|
89
|
+
// native memory surface. NEVER write into user-authored files.
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
registerAgentKind("my-agent", (cfg, dir) => new MyAgentAdapter(cfg.id, "my-agent", dir));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Users then declare it in `.loom/config.json`:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "id": "my-agent", "kind": "my-agent", "role": "executor", "options": {} }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`cfg.options` arrives in your constructor if you define one — see
|
|
102
|
+
`src/adapters/claude-code.ts` and `src/adapters/opencode.ts` for real examples,
|
|
103
|
+
including session persistence via project state.
|
|
104
|
+
|
|
105
|
+
### A bridge
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { BridgeBase, registerAgentKind } from "threadloom/sdk";
|
|
109
|
+
|
|
110
|
+
export class MyGuiBridge extends BridgeBase {
|
|
111
|
+
async available() { return true; }
|
|
112
|
+
async start() {
|
|
113
|
+
this.emit({ kind: "status", payload: { state: "observed" } });
|
|
114
|
+
}
|
|
115
|
+
async stop() {}
|
|
116
|
+
// injectMemory() default keeps .loom/memory/<id>.md fresh — the human
|
|
117
|
+
// driving the GUI agent points it at that file.
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerAgentKind("my-gui", (cfg, dir) => new MyGuiBridge(cfg.id, "my-gui", dir));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Ground rules
|
|
124
|
+
|
|
125
|
+
1. **Never block `send()` on user interaction** — emit `needs_input` and finish the turn.
|
|
126
|
+
2. **Interrupt must be safe** — a confirmed handoff interrupts you; leave the tree in a
|
|
127
|
+
sane state (that's why baton holders are adapters only).
|
|
128
|
+
3. **Emit `run_complete` exactly once per turn** — notifications and suggestions key off it.
|
|
129
|
+
4. **Respect namespacing** — your memory surface is `.loom/memory/<your-id>.md` and
|
|
130
|
+
whatever *additive* mechanism your agent offers. User files are off-limits.
|
|
131
|
+
5. **Persist session state** via project state (see `readProjectState` /
|
|
132
|
+
`writeProjectState`) so `--resume`-style continuity survives daemon restarts.
|