viewable-one 0.0.0-stage → 0.12.2
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/CHANGELOG.md +1483 -0
- package/LICENSE +21 -0
- package/README.md +738 -2
- package/SPAWN.md +2108 -0
- package/glytch.mjs +517 -0
- package/lib/acp.mjs +1018 -0
- package/lib/admission.mjs +105 -0
- package/lib/agent-selection.mjs +124 -0
- package/lib/agents.mjs +914 -0
- package/lib/atomic.mjs +96 -0
- package/lib/auth.mjs +1491 -0
- package/lib/authenticatedHttp.mjs +75 -0
- package/lib/boardside.mjs +225 -0
- package/lib/child.mjs +1292 -0
- package/lib/cli-output.mjs +176 -0
- package/lib/cloudflared-release.json +25 -0
- package/lib/cloudflared.mjs +81 -0
- package/lib/codex-rpc.mjs +92 -0
- package/lib/codex.mjs +767 -0
- package/lib/codexApps.mjs +344 -0
- package/lib/codexPlanWindow.mjs +84 -0
- package/lib/codexToolInputs.mjs +139 -0
- package/lib/codingChecks.mjs +55 -0
- package/lib/compose.mjs +76 -0
- package/lib/computerApps.mjs +110 -0
- package/lib/conversationRules.mjs +45 -0
- package/lib/detect.mjs +629 -0
- package/lib/devserver.mjs +670 -0
- package/lib/digest.mjs +290 -0
- package/lib/doctor.mjs +464 -0
- package/lib/exitClass.mjs +64 -0
- package/lib/hosted.mjs +112 -0
- package/lib/imageAttachments.mjs +31 -0
- package/lib/init-connect.mjs +418 -0
- package/lib/kill.mjs +104 -0
- package/lib/logindaemon.mjs +976 -0
- package/lib/logs.mjs +120 -0
- package/lib/mcpjson.mjs +242 -0
- package/lib/memberPreview.mjs +173 -0
- package/lib/multiwatch.mjs +428 -0
- package/lib/pagePictures.mjs +396 -0
- package/lib/pageRoutes.mjs +70 -0
- package/lib/paths.mjs +10 -0
- package/lib/permissionAsk.mjs +418 -0
- package/lib/picker.mjs +300 -0
- package/lib/platform.mjs +60 -0
- package/lib/previewGateway.mjs +130 -0
- package/lib/previewTunnel.mjs +85 -0
- package/lib/procs.mjs +428 -0
- package/lib/project-mcp.mjs +288 -0
- package/lib/projectPaths.mjs +31 -0
- package/lib/refusal.mjs +70 -0
- package/lib/registry.mjs +204 -0
- package/lib/repomap.mjs +323 -0
- package/lib/restartPass.mjs +151 -0
- package/lib/retired-address.mjs +78 -0
- package/lib/rowsAnswers.mjs +388 -0
- package/lib/rowsEvents.mjs +86 -0
- package/lib/run-engine.mjs +16 -0
- package/lib/runner-health.mjs +57 -0
- package/lib/runner.mjs +1640 -0
- package/lib/runnerAdmission.mjs +35 -0
- package/lib/runnerApi.mjs +312 -0
- package/lib/runnerCompletion.mjs +406 -0
- package/lib/runnerLease.mjs +37 -0
- package/lib/runnerMaintenance.mjs +27 -0
- package/lib/runnerPace.mjs +283 -0
- package/lib/runnerPerformance.mjs +26 -0
- package/lib/runnerRealtime.mjs +340 -0
- package/lib/runnerRegister.mjs +96 -0
- package/lib/runnerSettings.mjs +40 -0
- package/lib/runnerSettlement.mjs +145 -0
- package/lib/runnerWorkspace.mjs +177 -0
- package/lib/serverSweep.mjs +90 -0
- package/lib/sharedPreview.mjs +292 -0
- package/lib/signIn.mjs +47 -0
- package/lib/spawn.mjs +330 -0
- package/lib/sweep.mjs +161 -0
- package/lib/sync.mjs +484 -0
- package/lib/uninstall-project.mjs +589 -0
- package/lib/uninstall.mjs +464 -0
- package/lib/update.mjs +302 -0
- package/lib/usageLimit.mjs +108 -0
- package/lib/viewableFolder.mjs +105 -0
- package/lib/windows-command.mjs +85 -0
- package/lib/worktree.mjs +1142 -0
- package/lib/writePolicy.mjs +45 -0
- package/package.json +58 -4
- package/spawn-runner.mjs +167 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// GENERATED - DO NOT EDIT. Edit src/integrations/writePolicy.ts and run
|
|
2
|
+
// `node scripts/build-digest-copies.mjs`; src/digest/boardDigest.copy.test.ts
|
|
3
|
+
// fails the suite while the two disagree.
|
|
4
|
+
//
|
|
5
|
+
// A copy rather than an import for the reason every copy in this directory is:
|
|
6
|
+
// an Edge Function deploys as the files in its own directory. The source has
|
|
7
|
+
// no value imports of its own, so this is its transpile and nothing else: the
|
|
8
|
+
// rule is written once, where it belongs, and both the app and this function
|
|
9
|
+
// read the same one.
|
|
10
|
+
export function writeRisk(provider, name, args = {}) {
|
|
11
|
+
const words = `${name.replace(/([a-z])([A-Z])/g, "$1_$2")} ${String(args.method ?? args.action ?? args.command ?? "")}`.toLowerCase();
|
|
12
|
+
if (provider === "stripe" || /refund|payment|charge|transfer|payout|invoice|subscription/.test(words))
|
|
13
|
+
return "money";
|
|
14
|
+
if (/send|email|message|slack.*post/.test(words))
|
|
15
|
+
return "sending";
|
|
16
|
+
if (args.archived === true || args.in_trash === true || /delet|remov|destroy|archive|cancel|\breplace_content\b/.test(words))
|
|
17
|
+
return "deleting";
|
|
18
|
+
if (/publish|release/.test(words))
|
|
19
|
+
return "publishing";
|
|
20
|
+
if (/merge/.test(words))
|
|
21
|
+
return "merging";
|
|
22
|
+
if (/deploy/.test(words))
|
|
23
|
+
return "deploying";
|
|
24
|
+
// An explicit, small allowlist. A new tool is open-ended until reviewed.
|
|
25
|
+
if (provider === "linear" && ["create_issue", "update_issue", "save_issue"].includes(name))
|
|
26
|
+
return null;
|
|
27
|
+
if (provider === "notion" && ["notion-create-pages", "create_pages"].includes(name))
|
|
28
|
+
return null;
|
|
29
|
+
if (provider === "notion" && ["notion-update-page", "update_page"].includes(name)
|
|
30
|
+
&& [undefined, "insert_content", "update_properties", "replace_content_range"].includes(args.command))
|
|
31
|
+
return null;
|
|
32
|
+
if (provider === "github" && (name === "add_issue_comment" || (name === "issue_write" && ["create", "update"].includes(String(args.method)))))
|
|
33
|
+
return null;
|
|
34
|
+
if (["jira", "atlassian"].includes(provider) && ["createJiraIssue", "editJiraIssue"].includes(name))
|
|
35
|
+
return null;
|
|
36
|
+
return "open-ended";
|
|
37
|
+
}
|
|
38
|
+
export function riskReason(risk) {
|
|
39
|
+
if (risk === "money")
|
|
40
|
+
return "Money moves, so this asks every time.";
|
|
41
|
+
return `${risk === "open-ended" ? "Open-ended tools ask" : risk[0].toUpperCase() + risk.slice(1) + " asks"} every time.`;
|
|
42
|
+
}
|
|
43
|
+
export function writesSupported(provider) {
|
|
44
|
+
return ["notion", "linear", "github", "jira", "atlassian", "sentry", "stripe"].includes(provider);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viewable-one",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.12.2",
|
|
4
|
+
"description": "Run coding agents from the Viewable board. Connects your machine to app.viewable.one, launches a session in its own worktree when you press Run, and streams what it does back to the canvas.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"claude",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"canvas",
|
|
10
|
+
"board",
|
|
11
|
+
"agent",
|
|
12
|
+
"viewable"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/glytch-ai/viewable/tree/main/bridge#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/glytch-ai/viewable.git",
|
|
18
|
+
"directory": "bridge"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/glytch-ai/viewable/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"//bin": "viewable is the primary command; glytch and pm-spawn-runner remain compatible aliases (#2196, #2201).",
|
|
29
|
+
"bin": {
|
|
30
|
+
"viewable": "glytch.mjs",
|
|
31
|
+
"glytch": "glytch.mjs",
|
|
32
|
+
"pm-spawn-runner": "spawn-runner.mjs",
|
|
33
|
+
"viewable-one": "glytch.mjs"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"glytch.mjs",
|
|
37
|
+
"spawn-runner.mjs",
|
|
38
|
+
"lib/",
|
|
39
|
+
"README.md",
|
|
40
|
+
"SPAWN.md",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22"
|
|
46
|
+
},
|
|
47
|
+
"//dependencies": "ACP packages load only for the acp-claude engine. smol-toml validates Codex config before appending a board table; jsonc-parser edits OpenCode config while preserving comments. @supabase/realtime-js holds the runner's push channel (#813), reached only through lib/runnerRealtime.mjs and loaded on first use; it uses the WebSocket built into Node 22.",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@agentclientprotocol/claude-agent-acp": "0.73.0",
|
|
50
|
+
"@agentclientprotocol/sdk": "1.4.0",
|
|
51
|
+
"@supabase/realtime-js": "2.112.3",
|
|
52
|
+
"jsonc-parser": "3.3.1",
|
|
53
|
+
"smol-toml": "1.8.0"
|
|
54
|
+
},
|
|
55
|
+
"//scripts": "NO postinstall since 2026-09-14. It ran lib/installCloudflared.mjs to pre-fetch the cloudflared binary, which is only ever needed by a SHARED PREVIEW - so every install of the runner paid for a download most machines never use, and printed a warning line when it failed (reported through the app's feedback as an npm warning on install). lib/previewTunnel.mjs already calls ensureCloudflared lazily and reports its own failure in words a person can act on, so the fetch happens the first time a preview is actually shared and not before. prepublishOnly is NOT the test suite, on purpose: it used to be `node --test`, and a filesystem-race test that only misbehaves in one shell blocked about ten releases without ever catching a real defect in one. The suite runs in `npm run verify` and as its own step in the release workflow, both of which stop a publish. scripts/prepublish.mjs checks what is only true of the artifact and cannot answer differently twice.",
|
|
56
|
+
"scripts": {
|
|
57
|
+
"test": "node ../scripts/test-bridge.mjs",
|
|
58
|
+
"prepublishOnly": "node scripts/prepublish.mjs"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/spawn-runner.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The spawn runner's command line. bridge/SPAWN.md is the contract.
|
|
3
|
+
//
|
|
4
|
+
// This file exists so that turning the runner on is a THING A PERSON TYPES.
|
|
5
|
+
// Nothing imports it, nothing starts it, and no other part of the bridge can
|
|
6
|
+
// reach it: no session's board tools can start a runner however it is
|
|
7
|
+
// prompted. That is deliberate and it is the outermost of the money gates.
|
|
8
|
+
//
|
|
9
|
+
// PM_SPAWN_RUNNER=on node bridge/spawn-runner.mjs watch # actually spend
|
|
10
|
+
// node bridge/spawn-runner.mjs once # one pass
|
|
11
|
+
// node bridge/spawn-runner.mjs merge <worktree-path>
|
|
12
|
+
// node bridge/spawn-runner.mjs delete-worktree <worktree-path>
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
import { newRunnerId, pollOnce, settingsFromEnv, watch } from "./lib/runner.mjs";
|
|
16
|
+
import { AGENTS } from "./lib/agents.mjs";
|
|
17
|
+
import { makeAgentProbe } from "./lib/detect.mjs";
|
|
18
|
+
import { platformMark } from "./lib/platform.mjs";
|
|
19
|
+
import { mergeWorktree, deleteWorktree } from "./lib/worktree.mjs";
|
|
20
|
+
|
|
21
|
+
const root = process.env.PM_BOARD_ROOT?.trim() || process.cwd();
|
|
22
|
+
const say = (line = "") => process.stdout.write(`${line}\n`);
|
|
23
|
+
const die = (line) => {
|
|
24
|
+
process.stderr.write(`${line}\n`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
29
|
+
|
|
30
|
+
const USAGE = `pm spawn runner: runs Claude Code sessions the board asked for.
|
|
31
|
+
|
|
32
|
+
watch
|
|
33
|
+
Poll and run what is approved. Requires PM_SPAWN_RUNNER=on as well as
|
|
34
|
+
this command: an absent variable is not consent.
|
|
35
|
+
|
|
36
|
+
once
|
|
37
|
+
One pass instead of a loop. Same gate.
|
|
38
|
+
|
|
39
|
+
merge <worktree-path>
|
|
40
|
+
Merge a session's worktree branch into main and clean up.
|
|
41
|
+
|
|
42
|
+
delete-worktree <worktree-path>
|
|
43
|
+
Delete a worktree and its branch without merging.
|
|
44
|
+
|
|
45
|
+
RETIRED (brief 30 Move 2b, 2026-09-11): the mailbox these spoke is a row
|
|
46
|
+
now; reach supabase/functions/board-runner/ directly with a viewable login (or
|
|
47
|
+
VIEWABLE_BOARD_TOKEN) bearer instead:
|
|
48
|
+
|
|
49
|
+
request, approve, status, show, release
|
|
50
|
+
|
|
51
|
+
Environment:
|
|
52
|
+
PM_BOARD_ROOT the linked folder (default: cwd)
|
|
53
|
+
PM_SPAWN_RUNNER=on the switch; nothing runs without it
|
|
54
|
+
PM_CLAUDE_BIN the binary (default "claude"); point it at a fake
|
|
55
|
+
script to exercise every path for free
|
|
56
|
+
PM_CLAUDE_ARGS JSON array, "{prompt}" is substituted
|
|
57
|
+
PM_SPAWN_MAX_CONCURRENT how many sessions run at once; any number by default
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const RETIRED_COMMANDS = ["request", "approve", "status", "show", "release"];
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
if (RETIRED_COMMANDS.includes(command)) {
|
|
64
|
+
die(
|
|
65
|
+
`\`${command}\` is retired (brief 30 Move 2b, 2026-09-11): the file mailbox it spoke is gone, a run\n` +
|
|
66
|
+
`is a row now. Reach supabase/functions/board-runner/ directly with a viewable login (or\n` +
|
|
67
|
+
`VIEWABLE_BOARD_TOKEN) bearer for what this command used to do by hand.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
switch (command) {
|
|
72
|
+
case "watch":
|
|
73
|
+
case "once": {
|
|
74
|
+
const settings = { ...settingsFromEnv(), runnerId: newRunnerId() };
|
|
75
|
+
if (!settings.enabled) {
|
|
76
|
+
die(
|
|
77
|
+
"The runner is off.\n\n" +
|
|
78
|
+
"It has no default-on path and it does not start itself: spawning a session\n" +
|
|
79
|
+
"uses your own Claude Code or Codex plan, so an absent variable is not consent. Turn it on\n" +
|
|
80
|
+
"for one command, deliberately:\n\n" +
|
|
81
|
+
" PM_SPAWN_RUNNER=on node bridge/spawn-runner.mjs watch\n\n" +
|
|
82
|
+
"To try it without spending anything, point PM_CLAUDE_BIN at a fake binary\n" +
|
|
83
|
+
"first (bridge/test/fixtures/fake-claude.mjs is exactly that).",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
// THE WARNING MUST SURVIVE THE ENGINES BECOMING PLURAL. It used to be
|
|
87
|
+
// keyed on claudeBin === "claude", and when settings stopped prefilling
|
|
88
|
+
// that field - so a request could choose its own engine - the whole
|
|
89
|
+
// warning silently stopped firing. A safety notice that quietly turns
|
|
90
|
+
// itself off is worse than not having written one.
|
|
91
|
+
//
|
|
92
|
+
// So it asks the same question the runner will: what would actually be
|
|
93
|
+
// spawned. Overridden means a fake binary and no spend; not overridden
|
|
94
|
+
// means whatever real engine a request names.
|
|
95
|
+
if (settings.claudeBin === undefined) {
|
|
96
|
+
const names = Object.values(AGENTS)
|
|
97
|
+
.map((a) => `${a.label} (${a.binEnv})`)
|
|
98
|
+
.join(", ");
|
|
99
|
+
say(
|
|
100
|
+
`! This uses your own Claude Code or Codex plan: no binary override is set, so a request runs the real engine it asks for: ${names}.`,
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
say(
|
|
104
|
+
`Using ${settings.claudeBin} for every request - nothing comes out of your plan unless that is the real engine.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (command === "once") {
|
|
108
|
+
const pass = await pollOnce(root, settings, say);
|
|
109
|
+
for (const r of pass.ran) say(`[${r.id}] ${r.state}`);
|
|
110
|
+
for (const s of pass.skipped) say(`waiting: ${s.reason}`);
|
|
111
|
+
if (pass.ran.length === 0 && pass.skipped.length === 0) say("Nothing approved and pending.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
process.on("SIGINT", () => {
|
|
116
|
+
say("\nStopping. Any session already running keeps running, it is a separate process.");
|
|
117
|
+
controller.abort();
|
|
118
|
+
});
|
|
119
|
+
// WHAT THIS MACHINE HAS, in the presence a registered project carries
|
|
120
|
+
// (brief 19 stage 2, moved onto rows in brief 30 Move 1/2). The
|
|
121
|
+
// multi-root daemon does this in lib/multiwatch.mjs; a runner started
|
|
122
|
+
// here - by hand, or by `glytch runner watch` with PM_BOARD_ROOT set -
|
|
123
|
+
// has to say the same thing, or the app's presence would depend on
|
|
124
|
+
// WHICH way the runner was started. Probed once before the first beat
|
|
125
|
+
// and then on the probe's own five-minute clock; the beat itself only
|
|
126
|
+
// ever reads memory.
|
|
127
|
+
const probe = makeAgentProbe({ log: say });
|
|
128
|
+
await probe.once();
|
|
129
|
+
// AND WHAT MACHINE IT IS, for the same reason (brief 21 session 5, his
|
|
130
|
+
// ruling 3A): the app picks the detached-dev-server words from the
|
|
131
|
+
// platform the RUNNER reports, never from the browser's own, so a
|
|
132
|
+
// hand-started runner has to say it too or a session started this way
|
|
133
|
+
// would be handed the wrong shell.
|
|
134
|
+
const platform = platformMark();
|
|
135
|
+
await watch(
|
|
136
|
+
root,
|
|
137
|
+
{ ...settings, markExtra: () => ({ ...platform, ...probe.mark() }), refreshAgents: () => probe.once() },
|
|
138
|
+
{ signal: controller.signal, log: say },
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case "merge": {
|
|
144
|
+
const worktreePath = rest[0];
|
|
145
|
+
if (!worktreePath) die("Which worktree path?\n\n" + USAGE);
|
|
146
|
+
const result = await mergeWorktree(root, worktreePath, say);
|
|
147
|
+
if (!result.ok) die(result.detail);
|
|
148
|
+
say(result.detail);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "delete-worktree": {
|
|
153
|
+
const worktreePath = rest[0];
|
|
154
|
+
if (!worktreePath) die("Which worktree path?\n\n" + USAGE);
|
|
155
|
+
const result = await deleteWorktree(root, worktreePath, say);
|
|
156
|
+
if (!result.ok) die(result.detail);
|
|
157
|
+
say(result.detail);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
say(USAGE);
|
|
163
|
+
if (command !== undefined && command !== "help" && command !== "--help") process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
main().catch((error) => die(error instanceof Error ? error.message : String(error)));
|