viewable-one 0.0.0-stage → 0.12.1
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 +1460 -0
- package/LICENSE +21 -0
- package/README.md +755 -2
- package/SPAWN.md +2105 -0
- package/glytch.mjs +517 -0
- package/lib/acp.mjs +978 -0
- package/lib/admission.mjs +105 -0
- package/lib/agent-selection.mjs +123 -0
- package/lib/agents.mjs +913 -0
- package/lib/atomic.mjs +96 -0
- package/lib/auth.mjs +1387 -0
- package/lib/authenticatedHttp.mjs +68 -0
- package/lib/boardside.mjs +225 -0
- package/lib/child.mjs +1252 -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 +747 -0
- package/lib/codexApps.mjs +332 -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 +605 -0
- package/lib/devserver.mjs +659 -0
- package/lib/digest.mjs +290 -0
- package/lib/doctor.mjs +464 -0
- package/lib/exitClass.mjs +64 -0
- package/lib/glytchFolder.mjs +105 -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 +165 -0
- package/lib/multiwatch.mjs +428 -0
- package/lib/pagePictures.mjs +396 -0
- package/lib/pageRoutes.mjs +67 -0
- package/lib/paths.mjs +9 -0
- package/lib/permissionAsk.mjs +418 -0
- package/lib/picker.mjs +300 -0
- package/lib/platform.mjs +60 -0
- package/lib/previewGateway.mjs +129 -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 +322 -0
- package/lib/restartPass.mjs +151 -0
- package/lib/retired-address.mjs +78 -0
- package/lib/rowsAnswers.mjs +389 -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 +1569 -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 +138 -0
- package/lib/runnerWorkspace.mjs +162 -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 +588 -0
- package/lib/uninstall.mjs +457 -0
- package/lib/update.mjs +302 -0
- package/lib/usageLimit.mjs +108 -0
- package/lib/windows-command.mjs +85 -0
- package/lib/worktree.mjs +1138 -0
- package/lib/writePolicy.mjs +45 -0
- package/package.json +58 -4
- package/spawn-runner.mjs +167 -0
package/lib/compose.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// WHAT A FRESH CHILD'S PROMPT IS BUILT FROM, besides the prompt.
|
|
2
|
+
//
|
|
3
|
+
// Split out of runner.mjs on 2026-09-02, verbatim. Two things get prepended
|
|
4
|
+
// or appended to a FRESH session's prompt - never to a resume, which carries
|
|
5
|
+
// its own conversation memory - and both are composed at spawn time, with no
|
|
6
|
+
// model call:
|
|
7
|
+
//
|
|
8
|
+
// the repo primer (lib/repomap.mjs) - the tree and its exported symbols
|
|
9
|
+
// the attempts section (here) - what earlier runs of this same prompt tried
|
|
10
|
+
// and how they failed
|
|
11
|
+
//
|
|
12
|
+
// fromIdOf lives with the board half, because a session's questions hang off
|
|
13
|
+
// what it returns. Imported rather than re-derived: two derivations that
|
|
14
|
+
// drift put a session's questions on a different idea than its run, and
|
|
15
|
+
// nothing errors when they do.
|
|
16
|
+
import { fromIdOf } from "./boardside.mjs";
|
|
17
|
+
|
|
18
|
+
/** ROWS VERSION (brief 30 Move 2, 2026-09-11) of the section above. Same
|
|
19
|
+
* bounds, same shape, same "summaries only, never a previous prompt"
|
|
20
|
+
* reasoning - only the source changed, from `listRequests`/`readRequest`/
|
|
21
|
+
* `readSpawnReceipt`/`<id>.log` files to `runnerApi.runs`, which the Move 2
|
|
22
|
+
* contract already needs for maintenance and hands back every run's
|
|
23
|
+
* `request` and `result` on one project. There is no separate log tail to
|
|
24
|
+
* read here: `result` IS the receipt text a failed run settled with
|
|
25
|
+
* (`runnerCompletion.mjs`'s `patch.result`), which already carries the same
|
|
26
|
+
* closing detail the file version dug out of a `.log` tail. */
|
|
27
|
+
export async function buildAttemptsSectionFromRows(projectId, fromId, currentId, { runnerApi, env, log = () => {} } = {}) {
|
|
28
|
+
const MAX_SECTION = 6000;
|
|
29
|
+
const MAX_ATTEMPT = 700;
|
|
30
|
+
|
|
31
|
+
const listed = await runnerApi.runs({ projectIds: [projectId] }, { env });
|
|
32
|
+
if (!listed.ok) {
|
|
33
|
+
log(`[buildAttemptsSectionFromRows] could not list prior runs: ${listed.error}`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const priorAttempts = [];
|
|
37
|
+
for (const run of listed.runs ?? []) {
|
|
38
|
+
if (run.id === currentId) continue;
|
|
39
|
+
if (run.status !== "error") continue; // successes and still-open runs carry no attempt
|
|
40
|
+
const reqFromId = fromIdOf(run.request);
|
|
41
|
+
if (reqFromId !== fromId) continue;
|
|
42
|
+
const text = typeof run.result === "string" ? run.result : "";
|
|
43
|
+
const lines = text.split("\n").slice(3); // past the OK/ERROR line, the file-and-timestamp line, and the blank line
|
|
44
|
+
const summary = lines.join(" ").trim().slice(0, MAX_ATTEMPT);
|
|
45
|
+
priorAttempts.push({ id: run.id, summary });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (priorAttempts.length === 0) return null;
|
|
49
|
+
|
|
50
|
+
let section = "# Previous attempts\n\n";
|
|
51
|
+
section += "Earlier sessions for this prompt failed. Their summaries:\n\n";
|
|
52
|
+
|
|
53
|
+
const attempts = [];
|
|
54
|
+
let totalChars = section.length;
|
|
55
|
+
let dropped = 0;
|
|
56
|
+
|
|
57
|
+
for (let i = priorAttempts.length - 1; i >= 0; i--) {
|
|
58
|
+
const attempt = priorAttempts[i];
|
|
59
|
+
const attemptNum = priorAttempts.length - i;
|
|
60
|
+
const attemptText = `**Attempt ${attemptNum}** (${attempt.id}):\n${attempt.summary}\n\n`;
|
|
61
|
+
if (totalChars + attemptText.length > MAX_SECTION) {
|
|
62
|
+
dropped = i + 1;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
attempts.unshift(attemptText);
|
|
66
|
+
totalChars += attemptText.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (dropped > 0) {
|
|
70
|
+
section += `(${dropped} earlier attempt${dropped === 1 ? "" : "s"} omitted for space)\n\n`;
|
|
71
|
+
}
|
|
72
|
+
section += attempts.join("");
|
|
73
|
+
|
|
74
|
+
log(`[buildAttemptsSectionFromRows] Found ${priorAttempts.length} prior failure(s) for ${fromId}, included ${attempts.length}, dropped ${dropped}`);
|
|
75
|
+
return section;
|
|
76
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// #1132: invoked only after a claimed Look request. No model turn, file
|
|
2
|
+
// mutation or app tool call. Config contents never leave this process.
|
|
3
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { prepareCodexApps } from './codexApps.mjs';
|
|
8
|
+
import { execProbe, resolveAgentBinary } from './detect.mjs';
|
|
9
|
+
|
|
10
|
+
export async function lookForComputerApps({ cwd, env = process.env, platform = process.platform,
|
|
11
|
+
home = homedir(), read = readFile, probe = execProbe, codex = prepareCodexApps,
|
|
12
|
+
resolve = resolveAgentBinary } = {}) {
|
|
13
|
+
const apps = [];
|
|
14
|
+
const failed = new Set();
|
|
15
|
+
const add = (name, source, state) => {
|
|
16
|
+
if (typeof name !== 'string' || !name.trim() || name.length > 120 || /[\x00-\x1f\x7f]/.test(name)) return;
|
|
17
|
+
if (!apps.some((a) => a.name === name && a.source === source)) apps.push({ name, source, state });
|
|
18
|
+
};
|
|
19
|
+
const config = async (path, source, key = 'mcpServers') => {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await read(path, 'utf8');
|
|
22
|
+
if (raw.length > 2_000_000) throw new Error('too large');
|
|
23
|
+
const errors = [];
|
|
24
|
+
const data = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
25
|
+
if (errors.length) throw new Error("invalid");
|
|
26
|
+
const servers = data?.[key] ?? {};
|
|
27
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) throw new Error('invalid');
|
|
28
|
+
return { data, names: Object.keys(servers) };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error?.code !== 'ENOENT') failed.add(source);
|
|
31
|
+
return { data: {}, names: [] };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const claude = await config(join(home, '.claude.json'), 'Claude Code');
|
|
35
|
+
const project = await config(join(cwd, '.mcp.json'), 'Claude Code');
|
|
36
|
+
const names = [...new Set([...claude.names, ...project.names,
|
|
37
|
+
...Object.keys(claude.data?.projects?.[cwd]?.mcpServers ?? {})])];
|
|
38
|
+
const claudeBin = await resolve(env.PM_CLAUDE_BIN || 'claude', { env, platform });
|
|
39
|
+
if (names.length || claudeBin.found) {
|
|
40
|
+
const bin = claudeBin;
|
|
41
|
+
const checked = await probe(bin.path, ['mcp', 'list'], { cwd, env, platform, timeoutMs: 15_000 });
|
|
42
|
+
if (!checked.ok) failed.add('Claude Code');
|
|
43
|
+
const lines = checked.stdout.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').split(/\r?\n/);
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const match = /^([^:]{1,120}):.+(?:Connected|Failed to connect|Needs authentication)\s*$/.exec(line);
|
|
46
|
+
if (match && !names.includes(match[1])) names.push(match[1]);
|
|
47
|
+
}
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
const line = lines.find((line) => line.startsWith(`${name}:`));
|
|
50
|
+
add(name, 'Claude Code', checked.ok && /(?:✓|✔) Connected\s*$/.test(line ?? '') ? 'available' : 'unanswered');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const bin = await resolve(env.PM_CODEX_BIN || 'codex', { env, platform });
|
|
55
|
+
if (bin.found) {
|
|
56
|
+
const plan = await codex({ bin: bin.path, cwd, env, timeoutMs: 15_000 }, { on: true, isDraft: false });
|
|
57
|
+
for (const source of plan.sources) add(source.name, 'Codex', source.enabled && source.tools?.length > 0 ? 'available' : 'unanswered');
|
|
58
|
+
}
|
|
59
|
+
} catch { failed.add('Codex'); }
|
|
60
|
+
|
|
61
|
+
const support = platform === 'darwin' ? join(home, 'Library', 'Application Support')
|
|
62
|
+
: platform === 'win32' ? env.APPDATA || join(home, 'AppData', 'Roaming')
|
|
63
|
+
: env.XDG_CONFIG_HOME || join(home, '.config');
|
|
64
|
+
for (const [path, source, key] of [
|
|
65
|
+
[join(support, 'Claude', 'claude_desktop_config.json'), 'Claude Desktop', 'mcpServers'],
|
|
66
|
+
[join(support, 'Code', 'User', 'mcp.json'), 'VS Code', 'servers'],
|
|
67
|
+
[join(cwd, '.vscode', 'mcp.json'), 'VS Code', 'servers'],
|
|
68
|
+
[join(home, '.cursor', 'mcp.json'), 'Cursor', 'mcpServers'],
|
|
69
|
+
[join(cwd, '.cursor', 'mcp.json'), 'Cursor', 'mcpServers'],
|
|
70
|
+
]) {
|
|
71
|
+
const { names } = await config(path, source, key);
|
|
72
|
+
for (const name of names) add(name, source, 'unsupported');
|
|
73
|
+
}
|
|
74
|
+
if (apps.length > 200) throw new Error('Too many computer apps.');
|
|
75
|
+
return { apps, failed: [...failed] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Keep the names-only answer until acknowledged, independently of operation
|
|
79
|
+
// redelivery: claimed Looks no longer appear in the server's heartbeat ops.
|
|
80
|
+
// Scope by API, folder and project; expire superseded/unreachable receipts.
|
|
81
|
+
const pendingResults = new WeakMap();
|
|
82
|
+
|
|
83
|
+
export async function flushComputerApps(root, project, api, options) {
|
|
84
|
+
const pending = pendingResults.get(api);
|
|
85
|
+
let ok = true;
|
|
86
|
+
for (const [id, entry] of pending ?? []) {
|
|
87
|
+
if (entry.root !== root || entry.args.projectId !== project.id) continue;
|
|
88
|
+
if (entry.expires <= Date.now()) { pending.delete(id); continue; }
|
|
89
|
+
try {
|
|
90
|
+
const saved = await api.computerAppsResult(entry.args, options);
|
|
91
|
+
if (saved.ok) pending.delete(id);
|
|
92
|
+
else ok = false;
|
|
93
|
+
} catch { ok = false; }
|
|
94
|
+
}
|
|
95
|
+
return ok;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function serviceComputerApps(root, project, op, api, options, look = lookForComputerApps) {
|
|
99
|
+
if (op.project_id !== project.id) return false;
|
|
100
|
+
const args = { projectId: project.id, requestId: op.id };
|
|
101
|
+
const taken = await api.computerAppsTake(args, options);
|
|
102
|
+
if (!taken.ok || !taken.taken) return taken.ok;
|
|
103
|
+
let result;
|
|
104
|
+
try { result = await look({ cwd: root, env: options.env }); }
|
|
105
|
+
catch { result = { apps: [], failed: ['Claude Code', 'Codex', 'Claude Desktop', 'VS Code', 'Cursor'] }; }
|
|
106
|
+
let pending = pendingResults.get(api);
|
|
107
|
+
if (!pending) pendingResults.set(api, pending = new Map());
|
|
108
|
+
pending.set(op.id, { root, args: { ...args, result }, expires: Date.now() + 10 * 60_000 });
|
|
109
|
+
return flushComputerApps(root, project, api, options);
|
|
110
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// WHAT A SESSION IS TOLD ABOUT THE CONVERSATION ON ITS IDEA (brief 52,
|
|
2
|
+
// 2026-10-04), the tail of preambleFor in agents.mjs. The session is a voice
|
|
3
|
+
// in that conversation: a person's notes reach it as messages, by name; what it
|
|
4
|
+
// says back lands there as its own reply; and a picture of a visible change is
|
|
5
|
+
// worth more than a sentence about it. The closing reply is the runner's
|
|
6
|
+
// (boardside.mjs's reportOutcomeOnBoard files the closing words and the figures
|
|
7
|
+
// once), so the session must not file a second one. Its first sentence is the
|
|
8
|
+
// bold line of the result card (#2023), so it is asked for in those terms.
|
|
9
|
+
export function conversationRules(spec, ask) {
|
|
10
|
+
return (
|
|
11
|
+
"A person can write to you while you work. Their note arrives as " +
|
|
12
|
+
"`[message from <name>] <text>`. Answer them by calling " +
|
|
13
|
+
`${spec.boardTool("propose")} with an addComment op and no id: it lands in the ` +
|
|
14
|
+
"conversation on your own idea as your reply. Do that only for an answer " +
|
|
15
|
+
"the person is waiting on. When you finish, your closing message is filed " +
|
|
16
|
+
"for you, with the figures, and do not file a second reply of your own. " +
|
|
17
|
+
"Start it with one sentence of what changed, then at most three short " +
|
|
18
|
+
"lines as a `- ` list that someone who does not write code can read: no " +
|
|
19
|
+
"file names, no code.\n\n" +
|
|
20
|
+
"When you change something a person can see, attach a screenshot of it to " +
|
|
21
|
+
"an addComment (it takes `images`, up to 6 data URLs of PNG, JPEG, GIF or " +
|
|
22
|
+
`WebP). When you are choosing between looks, ask with ${ask} and pass ` +
|
|
23
|
+
"`images` so the person can see each one, and name them in the options.\n\n" +
|
|
24
|
+
"When you change a page of an app, write `.glytch-page.json` in the root " +
|
|
25
|
+
"of the folder you work in, naming that page's address and what a person " +
|
|
26
|
+
"calls it, like `{\"path\": \"/settings\", \"name\": \"settings page\"}`. " +
|
|
27
|
+
"The runner opens that page for its Before and After pictures; the file " +
|
|
28
|
+
"is never committed.\n\n"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// USE MY COMPUTER'S APPS IS OFF (#481). A run without the switch has no way
|
|
33
|
+
// to the person's own apps, so rather than hunt for one it says so in its
|
|
34
|
+
// closing message, in these words: the app finds the sentence and offers
|
|
35
|
+
// Open Manage agent under it.
|
|
36
|
+
export const APPS_OFF_SENTENCE = "Turn on **Use my computer's apps** in Manage agent and I will try again.";
|
|
37
|
+
export function computerAppsOffRule() {
|
|
38
|
+
return (
|
|
39
|
+
"You cannot reach the person's own apps from this run (their email, " +
|
|
40
|
+
"calendar, documents and the like). If the task needs one of them, do " +
|
|
41
|
+
"not look for a way round it: stop, and end your closing message with " +
|
|
42
|
+
"these two sentences, naming the app: \"I can't reach your <app> from " +
|
|
43
|
+
`here. ${APPS_OFF_SENTENCE}"\n\n`
|
|
44
|
+
);
|
|
45
|
+
}
|