@intentic/constants 1.360.0 → 1.362.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/README.md +2 -0
- package/package.json +7 -6
- package/src/hosted-tiers.ts +98 -0
- package/src/index.ts +147 -0
- package/src/memory-room.mjs +4 -30
- package/src/one-liner.ts +264 -0
- package/src/profile.ts +80 -0
- package/src/provider-logos.ts +26 -0
- package/src/turn-preamble.ts +28 -0
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ flowchart LR
|
|
|
14
14
|
- The index has no Node imports, so the web app and the public site bundle the same values the daemon reads.
|
|
15
15
|
It also carries the provider logos, the arrival profiles (`profile.ts`) and the hosted machine ladder
|
|
16
16
|
(`hosted-tiers.ts`), whose prices the site, the Billing page and the platform config all state.
|
|
17
|
+
- The package includes the TypeScript sources selected by `@intentic/src`, so pnpm's injected workspace copies
|
|
18
|
+
can load the index and its modules as well as the compiled exports.
|
|
17
19
|
- `turn-preamble.ts` is how a reader outside the sandbox takes the daemon's turn notes off a stored prompt: the
|
|
18
20
|
separator, the two dependency-notice openings, and `stripInjectedPreamble`. Session recall uses it. The daemon
|
|
19
21
|
builds its own notes with the same separator and openings, and its tests check every header it parses against it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/constants",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.362.0",
|
|
4
4
|
"description": "Shared constants for the intentic packages, ports, paths, and image references the daemon, CLIs and desktop app all agree on",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
|
+
"src/*.ts",
|
|
15
16
|
"src/node.mjs",
|
|
16
17
|
"src/node.d.mts",
|
|
17
18
|
"src/assertion-measure.mjs",
|
|
@@ -92,14 +93,14 @@
|
|
|
92
93
|
"devDependencies": {
|
|
93
94
|
"@intentic/testing": "0.0.0",
|
|
94
95
|
"@intentic/tsconfig": "0.0.0",
|
|
95
|
-
"@types/bun": "1.4.
|
|
96
|
-
"@types/node": "24.19.
|
|
97
|
-
"
|
|
96
|
+
"@types/bun": "1.4.4",
|
|
97
|
+
"@types/node": "24.19.2",
|
|
98
|
+
"typescript": "7.0.2"
|
|
98
99
|
},
|
|
99
100
|
"scripts": {
|
|
100
|
-
"build": "
|
|
101
|
+
"build": "tsc",
|
|
101
102
|
"typecheck": "bun check -p tsconfig.test.json",
|
|
102
|
-
"watch": "
|
|
103
|
+
"watch": "tsc --build --watch",
|
|
103
104
|
"test": "suites",
|
|
104
105
|
"test:watch": "suites --watch"
|
|
105
106
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// THE HOSTED MACHINE LADDER: every rung the platform runs, what it costs us to run, and what it sells for. One module
|
|
2
|
+
// because the platform's config defaults, the public site's copy, the Billing page and the economics test all state
|
|
3
|
+
// these figures, and a number typed in two places rots in one of them silently.
|
|
4
|
+
//
|
|
5
|
+
// What money buys here is a machine and nothing else: no capability, extension, automation or shared workspace is
|
|
6
|
+
// behind a rung. Every price below is Fly's published rate for the default region (iad); they move, and when they do
|
|
7
|
+
// the ladder's own test says which rung stopped adding up.
|
|
8
|
+
|
|
9
|
+
/** Fly's published volume rate, USD per provisioned GB per 30 days, charged whether the machine is awake or not. */
|
|
10
|
+
export const FLY_VOLUME_GB_USD = 0.15;
|
|
11
|
+
|
|
12
|
+
// Stripe's card fee on one monthly charge, as a share plus a fixed amount; what reaches us is the price less both.
|
|
13
|
+
export const STRIPE_FEE_SHARE = 0.029;
|
|
14
|
+
export const STRIPE_FEE_USD = 0.3;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What the free rung is allowed to cost us in its worst month, as a deliberate acquisition price rather than an
|
|
18
|
+
* accident. A change to free's shape or hours that breaks through this is a business decision, and the test says so.
|
|
19
|
+
*/
|
|
20
|
+
export const FREE_TIER_MONTHLY_CEILING_USD = 3.5;
|
|
21
|
+
|
|
22
|
+
/** Every rung there is, cheapest first, as a tuple so a schema can take it and the type cannot drift from the list. */
|
|
23
|
+
export const HOSTED_TIER_IDS = ["free", "standard", "max"] as const;
|
|
24
|
+
|
|
25
|
+
export type HostedTierId = (typeof HOSTED_TIER_IDS)[number];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The four numbers that make a machine, apart from which rung sold it. A machine row carries these rather than a tier
|
|
29
|
+
* id alone: the two diverge mid-migration, and after any edit to the ladder below.
|
|
30
|
+
*/
|
|
31
|
+
export interface HostedShape {
|
|
32
|
+
readonly cpuKind: "shared" | "performance";
|
|
33
|
+
readonly cpus: number;
|
|
34
|
+
readonly memoryMb: number;
|
|
35
|
+
readonly volumeGb: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HostedTier extends HostedShape {
|
|
39
|
+
readonly id: HostedTierId;
|
|
40
|
+
/** What the rung is called wherever a reader meets it. */
|
|
41
|
+
readonly name: string;
|
|
42
|
+
// Awake hours a month, enforced at wake; a sleeping machine spends none. Never 0: a rung with no ceiling cannot be
|
|
43
|
+
// shown to pay for itself, and tierMonthlyCostUsd would read as free.
|
|
44
|
+
readonly monthlyHours: number;
|
|
45
|
+
// Display only, and what the configured Stripe Price is checked against at boot; Stripe is what actually charges.
|
|
46
|
+
readonly priceUsd: number;
|
|
47
|
+
// Fly's published price for this exact guest, USD per awake hour, iad. Every cost figure derives from it.
|
|
48
|
+
readonly flyHourUsd: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* THE LADDER. Two facts decided these shapes, and they are worth knowing before moving one:
|
|
52
|
+
*
|
|
53
|
+
* 1. Extra shared vCPUs are nearly free once you are paying for RAM. shared-cpu-8x costs $8.07 a month more than
|
|
54
|
+
* shared-cpu-4x; the step from 4 GB to 8 GB costs $41.52. So both paid rungs go wide on CPU, which is rounding
|
|
55
|
+
* error, and are careful with memory and disk, which are not.
|
|
56
|
+
* 2. The free rung's cost is its disk, not its CPU. Forty awake hours on shared-cpu-4x is $1.32; its 10 GB volume is
|
|
57
|
+
* $1.50 a month standing, whether or not the person ever comes back. Dropping free to shared-cpu-2x would save
|
|
58
|
+
* $0.66 a month and halve the volume's ceiling (4000 IOPs and 16 MiB/s against 8000 and 32), which is exactly what
|
|
59
|
+
* a dependency install and an image pull wait on. */
|
|
60
|
+
export const HOSTED_TIERS: readonly HostedTier[] = [
|
|
61
|
+
{ id: "free", name: "Free", cpuKind: "shared", cpus: 4, memoryMb: 4096, volumeGb: 10, monthlyHours: 40, priceUsd: 0, flyHourUsd: 0.0329 },
|
|
62
|
+
{ id: "standard", name: "Standard", cpuKind: "shared", cpus: 8, memoryMb: 8192, volumeGb: 25, monthlyHours: 220, priceUsd: 20, flyHourUsd: 0.0657 },
|
|
63
|
+
{ id: "max", name: "Max", cpuKind: "shared", cpus: 8, memoryMb: 16_384, volumeGb: 50, monthlyHours: 320, priceUsd: 50, flyHourUsd: 0.1234 },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** The rung a machine is on when nobody has paid for it; also every hosted machine's starting shape. */
|
|
67
|
+
export const FREE_TIER: HostedTier = HOSTED_TIERS[0] as HostedTier;
|
|
68
|
+
|
|
69
|
+
/** Every rung that is sold, cheapest first: what the Stripe prices are checked against and what the site prices. */
|
|
70
|
+
export const PAID_TIERS: readonly HostedTier[] = HOSTED_TIERS.filter((tier) => tier.priceUsd > 0);
|
|
71
|
+
|
|
72
|
+
/** A machine as a reader meets it, from the machine's own numbers: "8 shared vCPUs · 8 GB memory · 25 GB disk". */
|
|
73
|
+
export const hostedShapeLine = (shape: HostedShape): string =>
|
|
74
|
+
`${shape.cpus} ${shape.cpuKind} vCPUs · ${shape.memoryMb / 1024} GB memory · ${shape.volumeGb} GB disk`;
|
|
75
|
+
|
|
76
|
+
export const isHostedTierId = (value: unknown): value is HostedTierId => HOSTED_TIER_IDS.includes(value as HostedTierId);
|
|
77
|
+
|
|
78
|
+
// Throws rather than answering undefined: the callers are a database column and a Stripe price, and an id neither
|
|
79
|
+
// recognises is corruption to be seen, not an absence to be handled.
|
|
80
|
+
export const hostedTier = (id: string): HostedTier => {
|
|
81
|
+
const tier = HOSTED_TIERS.find((rung) => rung.id === id);
|
|
82
|
+
if (tier === undefined) {
|
|
83
|
+
throw new Error(`no hosted tier named ${id}; the ladder is ${HOSTED_TIER_IDS.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
return tier;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** What a month on this rung costs us if its owner spends every awake hour it allows, plus the disk, which always runs. */
|
|
89
|
+
export const tierMonthlyCostUsd = (tier: HostedTier): number => tier.monthlyHours * tier.flyHourUsd + tier.volumeGb * FLY_VOLUME_GB_USD;
|
|
90
|
+
|
|
91
|
+
/** What reaches us from one month's charge after the card fee; 0 for a rung nobody pays for. */
|
|
92
|
+
export const tierNetRevenueUsd = (tier: HostedTier): number => (tier.priceUsd === 0 ? 0 : tier.priceUsd * (1 - STRIPE_FEE_SHARE) - STRIPE_FEE_USD);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether a machine moving from one rung to another needs a bigger disk. Fly volumes grow and never shrink, so a
|
|
96
|
+
* downgrade keeps the disk it has: the alternative is a file-level copy, and the rule is one sentence instead.
|
|
97
|
+
*/
|
|
98
|
+
export const tierDiskGrows = (from: HostedTier, to: HostedTier): boolean => to.volumeGb > from.volumeGb;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Cross-product constants with one source of truth, shared by the platform and the public intentic.dev site so
|
|
2
|
+
// hand-synced values can't drift.
|
|
3
|
+
|
|
4
|
+
export * from "./provider-logos.js";
|
|
5
|
+
export * from "./profile.js";
|
|
6
|
+
export * from "./hosted-tiers.js";
|
|
7
|
+
export * from "./turn-preamble.js";
|
|
8
|
+
|
|
9
|
+
// Fixed directory layouts shared across the package boundary; kept as plain values (no node:fs) since this module is
|
|
10
|
+
// importable from the browser. WORKSPACE_ROOT and HISTORY_ROOT are just defaults: a running daemon reads its actual
|
|
11
|
+
// roots from config, since an isolated turn re-points them.
|
|
12
|
+
|
|
13
|
+
// Project workspace dir inside the sandbox container; the three repos are cloned under <root>/<role>.
|
|
14
|
+
export const WORKSPACE_ROOT = "/work";
|
|
15
|
+
|
|
16
|
+
// Daemon-owned snapshot history, kept outside WORKSPACE_ROOT so a workspace-scoped `rm -rf` can't reach it.
|
|
17
|
+
export const HISTORY_ROOT = "/history";
|
|
18
|
+
|
|
19
|
+
// Daemon's own state folder; join through the state table's typed helpers instead, not by hand.
|
|
20
|
+
export const STATE_DIR = ".intentic";
|
|
21
|
+
|
|
22
|
+
// fileq's text shadows of workspace documents, one `<path>.md` per file at its mirrored path: written by fileq,
|
|
23
|
+
// kept current by the daemon's shadow keeper, read by iq and by the ripgrep preprocessor as the text of the original.
|
|
24
|
+
export const DERIVED_SHADOWS_DIR = `${STATE_DIR}/local/cache/derived`;
|
|
25
|
+
|
|
26
|
+
// The documents whose shadow stands in for their bytes when searched (iq, ripgrep): fileq's document formats, by
|
|
27
|
+
// extension, lower case. fileq's own table (formats.ts DOCUMENT_FORMATS) is held to this one by a test.
|
|
28
|
+
export const SHADOWED_DOCUMENT_EXTENSIONS: readonly string[] = [
|
|
29
|
+
".docx",
|
|
30
|
+
".epub",
|
|
31
|
+
".odg",
|
|
32
|
+
".odp",
|
|
33
|
+
".ods",
|
|
34
|
+
".odt",
|
|
35
|
+
".otp",
|
|
36
|
+
".ots",
|
|
37
|
+
".ott",
|
|
38
|
+
".pdf",
|
|
39
|
+
".pptx",
|
|
40
|
+
".rtf",
|
|
41
|
+
".xlsx",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Reserved top-level shelf for reference material: consultation-only, so every part that walks the workspace — the
|
|
45
|
+
// tree, search, dependency setup, file sync — skips it. A repository's own `refs/` deeper in is ordinary content, so
|
|
46
|
+
// match it by first segment (@intentic/workspace-ignore's isReferencePath), never by name at any depth.
|
|
47
|
+
export const REFERENCE_DIR = "refs";
|
|
48
|
+
|
|
49
|
+
// Reserved top-level outbox: everything in it is served on the public internet, and its existence alone is the switch.
|
|
50
|
+
export const PUBLIC_DIR = "public";
|
|
51
|
+
|
|
52
|
+
// The owner's standing instructions, one filename for every runtime: the daemon reads it and composes it into each
|
|
53
|
+
// turn's instructions itself, so no loop's own discovery decides which rules a turn ran under. One per folder, read
|
|
54
|
+
// from the workspace root down to where the turn starts.
|
|
55
|
+
export const MEMORY_FILE = "AGENTS.md";
|
|
56
|
+
|
|
57
|
+
// The sandbox's own field notes: what past sessions here had to learn the hard way, ranked, rewritten monthly by an
|
|
58
|
+
// automation rather than by hand. Sits with the owner's other composed-into-the-prompt file (safety.md) rather than at
|
|
59
|
+
// the workspace root, because nothing outside this product reads it.
|
|
60
|
+
export const FIELD_NOTES_FILE = ".intentic/config/field-notes.toon";
|
|
61
|
+
|
|
62
|
+
// Per-service state root on a provisioned host reached over ssh, as opposed to inside a sandbox container.
|
|
63
|
+
export const HOST_STATE_ROOT = "/opt/intentic";
|
|
64
|
+
|
|
65
|
+
// Clickwrap version stamped at sign-up; bump on any material change to the terms or privacy policy.
|
|
66
|
+
export const LEGAL_VERSION = "2026-10-15";
|
|
67
|
+
export const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
|
|
68
|
+
|
|
69
|
+
// Legal-entity identification EU e-commerce law requires published on the site. ADDRESS and TAX_ID are deliberately
|
|
70
|
+
// blank until set: an empty field omits the line, a wrong one would not.
|
|
71
|
+
export const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
|
|
72
|
+
export const LEGAL_ENTITY_COUNTRY = "Poland";
|
|
73
|
+
export const LEGAL_ENTITY_ADDRESS = "";
|
|
74
|
+
export const LEGAL_ENTITY_TAX_ID = "";
|
|
75
|
+
|
|
76
|
+
// Platform's own server location; blank leaves the privacy policy silent instead of guessing.
|
|
77
|
+
export const PLATFORM_HOSTING_LOCATION = "";
|
|
78
|
+
|
|
79
|
+
// Not a convenience default: a wildcard would let any page enumerate the loopback port off /health.
|
|
80
|
+
export const PLATFORM_WEB_ORIGIN = "https://app.intentic.dev";
|
|
81
|
+
|
|
82
|
+
// Public site origin; also where install one-liners (`curl ... | sh`) fetch their script from.
|
|
83
|
+
export const PLATFORM_SITE_ORIGIN = "https://intentic.dev";
|
|
84
|
+
|
|
85
|
+
// The community server's permanent, non-expiring invite: questions and feedback go here, bugs to GitHub Issues. One
|
|
86
|
+
// value for the site's links and the editor's invitation, since Discord has no stable /channels/ address to fall back on.
|
|
87
|
+
export const COMMUNITY_DISCORD_URL = "https://discord.gg/3veuzYp32T";
|
|
88
|
+
|
|
89
|
+
// One table for what three places once hand-typed separately: `path` is what a one-liner fetches, `file` is the asset
|
|
90
|
+
// under INSTALL_SCRIPTS_DIR. /rebuild and /update deliberately share one file.
|
|
91
|
+
export const INSTALL_SCRIPTS_DIR = "_site/site/public/scripts";
|
|
92
|
+
|
|
93
|
+
export const INSTALL_SCRIPTS = {
|
|
94
|
+
sh: { path: "/connect", file: "connect.sh" },
|
|
95
|
+
ps1: { path: "/connect.ps1", file: "connect.ps1" },
|
|
96
|
+
// "device", not "host": /connect-host enrolls a deploy target, /device connects the machine the user is on.
|
|
97
|
+
hostSh: { path: "/connect-host", file: "connect-host.sh" },
|
|
98
|
+
hostPs1: { path: "/connect-host.ps1", file: "connect-host.ps1" },
|
|
99
|
+
cleanupHost: { path: "/cleanup-host", file: "cleanup-host.sh" },
|
|
100
|
+
desktopSh: { path: "/sync", file: "sync.sh" },
|
|
101
|
+
desktopPs1: { path: "/sync.ps1", file: "sync.ps1" },
|
|
102
|
+
deviceSh: { path: "/device", file: "device.sh" },
|
|
103
|
+
devicePs1: { path: "/device.ps1", file: "device.ps1" },
|
|
104
|
+
rebuild: { path: "/rebuild", file: "recreate.sh" },
|
|
105
|
+
rebuildPs1: { path: "/rebuild.ps1", file: "recreate.ps1" },
|
|
106
|
+
update: { path: "/update", file: "recreate.sh" },
|
|
107
|
+
updatePs1: { path: "/update.ps1", file: "recreate.ps1" },
|
|
108
|
+
cleanup: { path: "/cleanup", file: "cleanup.sh" },
|
|
109
|
+
cleanupPs1: { path: "/cleanup.ps1", file: "cleanup.ps1" },
|
|
110
|
+
// The one command a sandbox that stopped answering hands out: `ic sandbox fix`, which checks every layer of the
|
|
111
|
+
// machine it runs on, heals what is safe and asks before anything else.
|
|
112
|
+
fix: { path: "/fix", file: "fix.sh" },
|
|
113
|
+
fixPs1: { path: "/fix.ps1", file: "fix.ps1" },
|
|
114
|
+
} as const satisfies Record<string, { path: string; file: string }>;
|
|
115
|
+
|
|
116
|
+
export type InstallScript = keyof typeof INSTALL_SCRIPTS;
|
|
117
|
+
|
|
118
|
+
/** Public URL a one-liner fetches, and what a reader can open to read the script first. */
|
|
119
|
+
export const installScriptUrl = (key: InstallScript): string => `${PLATFORM_SITE_ORIGIN}${INSTALL_SCRIPTS[key].path}`;
|
|
120
|
+
|
|
121
|
+
/** Script's path in the checkout, for local dev that runs the working tree instead of the published copy. */
|
|
122
|
+
export const installScriptPath = (key: InstallScript): string => `${INSTALL_SCRIPTS_DIR}/${INSTALL_SCRIPTS[key].file}`;
|
|
123
|
+
|
|
124
|
+
// The verb routes over the same scripts (`https://intentic.dev/<verb>/<arg>`): which verbs exist, what each argument
|
|
125
|
+
// may be, and what it becomes in the script. Its functions read the two values above only when called.
|
|
126
|
+
export * from "./one-liner.js";
|
|
127
|
+
|
|
128
|
+
// Every port bound inside the container is declared here and asserted distinct, so two services can never silently
|
|
129
|
+
// collide.
|
|
130
|
+
export const DAEMON_PORT = 8787;
|
|
131
|
+
export const PREVIEW_PORT = 5173;
|
|
132
|
+
|
|
133
|
+
// Loopback listener, published to the host; must be TLS since Safari blocks mixed content.
|
|
134
|
+
export const LOCAL_PORT = 8788;
|
|
135
|
+
|
|
136
|
+
// Bundled translator port, loopback-only; the Dockerfile bakes this value into TRANSLATOR_URL.
|
|
137
|
+
export const TRANSLATOR_PORT = 8789;
|
|
138
|
+
|
|
139
|
+
// The credential gateway, bound by the daemon on loopback only and never relayed by netd, so an address it issues is
|
|
140
|
+
// useless anywhere but inside this container (sandbox src/broker/).
|
|
141
|
+
export const BROKER_PORT = 8790;
|
|
142
|
+
|
|
143
|
+
// Public web client id, not secret; four places must agree on it or sign-in silently breaks.
|
|
144
|
+
export const GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
|
|
145
|
+
|
|
146
|
+
// localStorage slot for the cached ID token, keyed by client id so a rotation can't reuse it.
|
|
147
|
+
export const GOOGLE_TOKEN_STORAGE_KEY = `intentic.gid.${GOOGLE_CLIENT_ID}`;
|
package/src/memory-room.mjs
CHANGED
|
@@ -1,34 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// imports it by path before any install, and `memory-room` runs it as a command.
|
|
7
|
-
//
|
|
8
|
-
// limit memory.high (where the kernel starts throttling the whole cgroup), else memory.max, else the machine's
|
|
9
|
-
// memory, never more than the machine's
|
|
10
|
-
// used the working set (memory.current less the inactive file cache the kernel reclaims first) plus what was
|
|
11
|
-
// pushed to swap; at a root cgroup, which has no memory.current, the machine's own used memory and swap
|
|
12
|
-
// free limit − the resident part of used, and never more than the machine itself has available (MemAvailable): a
|
|
13
|
-
// sandbox on a shared machine cannot take memory the machine does not have, whatever its own limit says.
|
|
14
|
-
// Swap is not subtracted: the limit bounds resident pages only, and cold pages parked in swap leave that room
|
|
15
|
-
// free. Counting them held every heavy command for minutes on a sandbox with 8 GB available (measured
|
|
16
|
-
// 2026-09-26..10-02: 4.2 GiB of idle swap, 17 h of gate waits a week). Swap that is being paged back in shows
|
|
17
|
-
// as stall, below, which holds work whatever the bytes say.
|
|
18
|
-
// Once swap is nearly full (SWAP_FULL_SHARE of memory.swap.max, or of what the machine's swap can still take)
|
|
19
|
-
// that stops being true: nothing more can be parked there, so a page the work touches comes back only by
|
|
20
|
-
// pushing out another, and the swapped pages count against the limit with the resident ones. Measured
|
|
21
|
-
// 2026-10-05: 12 GiB resident + 10 of 10 GiB swapped against a 20 GiB limit, load 38, a third of the time
|
|
22
|
-
// waiting on disk, and the formula still read 2.4 GiB free and started every turn.
|
|
23
|
-
// stall memory PSI `full`: avg10, the share of the last ten seconds in which everything waited on memory, and avg60,
|
|
24
|
-
// the last minute's. A sandbox paging hard flickers around any avg10 threshold (19, 11, 15, 4, 18% in five
|
|
25
|
-
// minutes of the reading above, never 20), so a lower bar on the minute catches what the spikes miss.
|
|
26
|
-
// disk io PSI `full` the same way, for work nobody is waiting on only: swap paged in from disk and a build's writes
|
|
27
|
-
// stall everything on the disk, the daemon's own event loop included, before memory PSI says much. Measured
|
|
28
|
-
// 2026-10-10..13: of 29 daemon freezes of 5 s or more, 20 had disk `full` avg10 at 30% or past it, 18 had
|
|
29
|
-
// memory's at 20%, and the one that failed a turn's setup on 2026-10-13 had disk 42% and memory 7%. Disk
|
|
30
|
-
// reached 30% in 1.5% of the minute samples of those days, so holding background work then costs little. A
|
|
31
|
-
// person's turn is never refused for it: a busy disk slows a turn, it does not kill one.
|
|
2
|
+
// Shared admission formula for ResourceBudget and scripts without a daemon; dependency-free for pre-install use.
|
|
3
|
+
// Limit is memory.high, else memory.max, else host RAM, capped at host RAM; free resident room is also capped at MemAvailable.
|
|
4
|
+
// Exclude reclaimable inactive cache; count swapped pages only near full swap. Memory full PSI avg10/avg60 catches paging pressure.
|
|
5
|
+
// I/O full PSI holds background work only: a busy disk may slow a person's turn but must not refuse it.
|
|
32
6
|
import { spawnSync } from "node:child_process";
|
|
33
7
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
34
8
|
import { request } from "node:http";
|
package/src/one-liner.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// THE ONE-LINER: every command a person copy-pastes is `curl -fsSL https://intentic.dev/<verb>/<arg> | sh` or
|
|
2
|
+
// `irm https://intentic.dev/<verb>/<arg> | iex`. The site's Worker serves the static script for the verb with the
|
|
3
|
+
// arguments injected; the editor and the sandbox build the URL. This table is the one place that says which verbs
|
|
4
|
+
// exist, what each argument may be, and which script and parameters it becomes, so the Worker that serves a URL and
|
|
5
|
+
// the code that hands it out cannot disagree.
|
|
6
|
+
//
|
|
7
|
+
// Every argument is checked against its own pattern, and every value injected into a script only ever holds
|
|
8
|
+
// `[A-Za-z0-9._~/-]`, so single-quoting it is safe in both shells.
|
|
9
|
+
|
|
10
|
+
import { INSTALL_SCRIPTS_DIR, PLATFORM_SITE_ORIGIN } from "./index.js";
|
|
11
|
+
|
|
12
|
+
export type OneLinerShell = "sh" | "ps1";
|
|
13
|
+
|
|
14
|
+
// Argument patterns, one per kind of value a URL segment can carry.
|
|
15
|
+
export const SETUP_CODE_PATTERN = /^[A-Za-z0-9]{6,64}$/;
|
|
16
|
+
// The leaf of the default sync folder, `~/intentic/<leaf>`: the folder itself never goes through the platform, only
|
|
17
|
+
// its last segment rides the URL into the script on the person's own device.
|
|
18
|
+
export const SYNC_LEAF_PATTERN = /^[a-z0-9][a-z0-9-]{0,80}$/;
|
|
19
|
+
// Never a leading `-`: fix.sh hands an argument that starts with one to `ic sandbox fix` as a flag, so `/fix/--yes`
|
|
20
|
+
// would run every fix without asking.
|
|
21
|
+
export const FIX_CODE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
|
|
22
|
+
export const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
23
|
+
export const IMAGE_HASH_PATTERN = /^[a-f0-9]{8,64}$/;
|
|
24
|
+
// A sandbox-minted short code (Crockford base32, 8 characters, one optional hyphen in the middle).
|
|
25
|
+
export const SHORT_CODE_PATTERN = /^[0-9A-Za-z]{4}-?[0-9A-Za-z]{4}$/;
|
|
26
|
+
// A bare DNS hostname: no port, no scheme. IP literals are refused separately (an all-digit last label).
|
|
27
|
+
export const HOSTNAME_PATTERN = /^(?=.{4,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
28
|
+
|
|
29
|
+
// The only characters an injected value may hold, whatever the verb: a second line of defence behind each pattern.
|
|
30
|
+
export const SAFE_ARG = /^[A-Za-z0-9._~/-]+$/;
|
|
31
|
+
|
|
32
|
+
/** The folder the setup page syncs by default, and the only kind whose leaf can ride a setup one-liner. */
|
|
33
|
+
export const DEFAULT_SYNC_ROOT = "~/intentic";
|
|
34
|
+
|
|
35
|
+
/** Whether `host` is a hostname a device/sync/server one-liner can carry (lowercase it first). */
|
|
36
|
+
export const isOneLinerHost = (host: string): boolean => HOSTNAME_PATTERN.test(host) && !/^\d+$/.test(host.slice(host.lastIndexOf(".") + 1));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A sandbox's address as a device, sync or server one-liner carries it: the bare hostname of its https URL. Undefined
|
|
40
|
+
* for one no one-liner can reach (localhost, an IP, a port), which keeps its environment-variable command.
|
|
41
|
+
*/
|
|
42
|
+
export const oneLinerHostOf = (sandboxUrl: string | undefined | null): string | undefined => {
|
|
43
|
+
if (sandboxUrl === undefined || sandboxUrl === null || sandboxUrl === ``) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!URL.canParse(sandboxUrl)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const url = new URL(sandboxUrl);
|
|
50
|
+
const host = url.hostname.toLowerCase();
|
|
51
|
+
return url.protocol === `https:` && url.port === `` && isOneLinerHost(host) ? host : undefined;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
interface ArgSpec {
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly test: (value: string) => boolean;
|
|
57
|
+
// Applied before the test, both by the Worker and by the builders (a hostname is case-insensitive).
|
|
58
|
+
readonly normalize?: (value: string) => string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A PowerShell parameter: `-Name 'value'`, or a bare switch.
|
|
62
|
+
export interface Ps1Param {
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly value?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a verb and its arguments become: the script files, sh's positional args and env, PowerShell's parameters. */
|
|
68
|
+
export interface ResolvedOneLiner {
|
|
69
|
+
readonly files: { readonly sh: string; readonly ps1: string };
|
|
70
|
+
readonly shArgs: readonly string[];
|
|
71
|
+
readonly shEnv: readonly (readonly [name: string, value: string])[];
|
|
72
|
+
readonly ps1Params: readonly Ps1Param[];
|
|
73
|
+
// Whether the arguments a person adds by hand (`| sh -s -- --dry-run`, or after the scriptblock in PowerShell) ride
|
|
74
|
+
// after the URL's. Otherwise the URL's replace them, so `$1…` are exactly what the URL says.
|
|
75
|
+
readonly typed: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface VerbSpec {
|
|
79
|
+
readonly files: { readonly sh: string; readonly ps1: string };
|
|
80
|
+
readonly required: readonly ArgSpec[];
|
|
81
|
+
readonly optional: readonly ArgSpec[];
|
|
82
|
+
readonly typed?: true;
|
|
83
|
+
readonly sh: (args: readonly string[]) => { readonly args: readonly string[]; readonly env?: readonly (readonly [string, string])[] };
|
|
84
|
+
readonly ps1: (args: readonly string[]) => readonly Ps1Param[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const arg = (name: string, pattern: RegExp, normalize?: (value: string) => string): ArgSpec => ({
|
|
88
|
+
name,
|
|
89
|
+
test: (value) => pattern.test(value),
|
|
90
|
+
...(normalize === undefined ? {} : { normalize }),
|
|
91
|
+
});
|
|
92
|
+
const slug = arg(`sandbox name`, SLUG_PATTERN);
|
|
93
|
+
const host: ArgSpec = { name: `sandbox address`, test: isOneLinerHost, normalize: (value) => value.toLowerCase() };
|
|
94
|
+
const shortCode = arg(`code`, SHORT_CODE_PATTERN);
|
|
95
|
+
|
|
96
|
+
const RECREATE = { sh: `recreate.sh`, ps1: `recreate.ps1` } as const;
|
|
97
|
+
|
|
98
|
+
// A device, sync or server connect: the sandbox's address and the code it minted, which the script redeems there.
|
|
99
|
+
const coded = (sh: string, ps1: string): VerbSpec => ({
|
|
100
|
+
files: { sh, ps1 },
|
|
101
|
+
required: [host, shortCode],
|
|
102
|
+
optional: [],
|
|
103
|
+
sh: ([address = ``, code = ``]) => ({ args: [address, code] }),
|
|
104
|
+
ps1: ([address = ``, code = ``]) => [
|
|
105
|
+
{ name: `Sandbox`, value: address },
|
|
106
|
+
{ name: `Code`, value: code },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const VERBS = {
|
|
111
|
+
setup: {
|
|
112
|
+
files: { sh: `connect.sh`, ps1: `connect.ps1` },
|
|
113
|
+
required: [arg(`setup code`, SETUP_CODE_PATTERN)],
|
|
114
|
+
optional: [arg(`sync folder`, SYNC_LEAF_PATTERN)],
|
|
115
|
+
sh: ([code = ``, leaf]) => ({ args: [code], env: leaf === undefined ? [] : [[`SYNC_DIR`, `${DEFAULT_SYNC_ROOT}/${leaf}`]] }),
|
|
116
|
+
ps1: ([code = ``, leaf]) => [
|
|
117
|
+
{ name: `SetupCode`, value: code },
|
|
118
|
+
...(leaf === undefined ? [] : [{ name: `SyncDir`, value: `${DEFAULT_SYNC_ROOT}/${leaf}` }]),
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
fix: {
|
|
122
|
+
files: { sh: `fix.sh`, ps1: `fix.ps1` },
|
|
123
|
+
required: [arg(`fix code`, FIX_CODE_PATTERN)],
|
|
124
|
+
optional: [],
|
|
125
|
+
sh: ([code = ``]) => ({ args: [code] }),
|
|
126
|
+
ps1: ([code = ``]) => [{ name: `Code`, value: code }],
|
|
127
|
+
},
|
|
128
|
+
update: {
|
|
129
|
+
files: RECREATE,
|
|
130
|
+
required: [slug],
|
|
131
|
+
optional: [],
|
|
132
|
+
sh: ([name = ``]) => ({ args: [name] }),
|
|
133
|
+
ps1: ([name = ``]) => [{ name: `Slug`, value: name }],
|
|
134
|
+
},
|
|
135
|
+
rollback: {
|
|
136
|
+
files: RECREATE,
|
|
137
|
+
required: [slug],
|
|
138
|
+
optional: [],
|
|
139
|
+
sh: ([name = ``]) => ({ args: [name, `--rollback`] }),
|
|
140
|
+
ps1: ([name = ``]) => [{ name: `Slug`, value: name }, { name: `Rollback` }],
|
|
141
|
+
},
|
|
142
|
+
restart: {
|
|
143
|
+
files: RECREATE,
|
|
144
|
+
required: [slug],
|
|
145
|
+
optional: [],
|
|
146
|
+
sh: ([name = ``]) => ({ args: [name, `--restart`] }),
|
|
147
|
+
ps1: ([name = ``]) => [{ name: `Slug`, value: name }, { name: `Restart` }],
|
|
148
|
+
},
|
|
149
|
+
rebuild: {
|
|
150
|
+
files: RECREATE,
|
|
151
|
+
required: [slug, arg(`image hash`, IMAGE_HASH_PATTERN)],
|
|
152
|
+
optional: [],
|
|
153
|
+
sh: ([name = ``, hash = ``]) => ({ args: [name, hash] }),
|
|
154
|
+
ps1: ([name = ``, hash = ``]) => [
|
|
155
|
+
{ name: `Slug`, value: name },
|
|
156
|
+
{ name: `Hash`, value: hash },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
// Recoverable for 7 days; with no name the script lists the sandboxes on the device and asks which.
|
|
160
|
+
remove: {
|
|
161
|
+
files: { sh: `cleanup.sh`, ps1: `cleanup.ps1` },
|
|
162
|
+
required: [],
|
|
163
|
+
optional: [slug],
|
|
164
|
+
sh: ([name]) => ({ args: name === undefined ? [] : [name, `--yes`] }),
|
|
165
|
+
ps1: ([name]) => (name === undefined ? [] : [{ name: `Slug`, value: name }, { name: `Yes` }]),
|
|
166
|
+
},
|
|
167
|
+
// Intentic itself off this device, by `ic uninstall`, which shows what it found and asks before it deletes
|
|
168
|
+
// anything. Nothing rides the URL; a flag the person adds (`--dry-run`, `--keep-sandboxes`) reaches ic.
|
|
169
|
+
uninstall: {
|
|
170
|
+
files: { sh: `uninstall.sh`, ps1: `uninstall.ps1` },
|
|
171
|
+
required: [],
|
|
172
|
+
optional: [],
|
|
173
|
+
typed: true,
|
|
174
|
+
sh: () => ({ args: [] }),
|
|
175
|
+
ps1: () => [],
|
|
176
|
+
},
|
|
177
|
+
device: coded(`device.sh`, `device.ps1`),
|
|
178
|
+
sync: coded(`sync.sh`, `sync.ps1`),
|
|
179
|
+
server: coded(`connect-host.sh`, `connect-host.ps1`),
|
|
180
|
+
} as const satisfies Record<string, VerbSpec>;
|
|
181
|
+
|
|
182
|
+
export type OneLinerVerb = keyof typeof VERBS;
|
|
183
|
+
|
|
184
|
+
export const ONE_LINER_VERBS: readonly OneLinerVerb[] = Object.keys(VERBS) as OneLinerVerb[];
|
|
185
|
+
|
|
186
|
+
export const isOneLinerVerb = (value: string): value is OneLinerVerb => Object.hasOwn(VERBS, value);
|
|
187
|
+
|
|
188
|
+
const article = (noun: string): string => (/^[aeiou]/.test(noun) ? `an` : `a`);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Checks a verb's arguments and says what they become, or why not in one sentence. The Worker serves nothing on an
|
|
192
|
+
* error; the builders fall back to a command the table cannot express.
|
|
193
|
+
*/
|
|
194
|
+
export const resolveOneLiner = (verb: OneLinerVerb, raw: readonly string[]): ResolvedOneLiner | { readonly error: string } => {
|
|
195
|
+
const spec: VerbSpec = VERBS[verb];
|
|
196
|
+
const most = spec.required.length + spec.optional.length;
|
|
197
|
+
if (raw.length < spec.required.length || raw.length > most) {
|
|
198
|
+
const wanted = [...spec.required.map((a) => `${article(a.name)} ${a.name}`), ...spec.optional.map((a) => `an optional ${a.name}`)];
|
|
199
|
+
return { error: wanted.length === 0 ? `${verb} takes nothing after it.` : `${verb} takes ${wanted.join(` and `)}.` };
|
|
200
|
+
}
|
|
201
|
+
const specs = [...spec.required, ...spec.optional];
|
|
202
|
+
const args: string[] = [];
|
|
203
|
+
for (const [index, value] of raw.entries()) {
|
|
204
|
+
const which = specs[index]!;
|
|
205
|
+
const normalized = which.normalize === undefined ? value : which.normalize(value);
|
|
206
|
+
if (!which.test(normalized) || !SAFE_ARG.test(normalized)) {
|
|
207
|
+
return { error: `That is not a valid ${which.name} for ${verb}.` };
|
|
208
|
+
}
|
|
209
|
+
args.push(normalized);
|
|
210
|
+
}
|
|
211
|
+
const sh = spec.sh(args);
|
|
212
|
+
return { files: spec.files, shArgs: sh.args, shEnv: sh.env ?? [], ps1Params: spec.ps1(args), typed: spec.typed === true };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const resolved = (verb: OneLinerVerb, args: readonly string[]): ResolvedOneLiner => {
|
|
216
|
+
const answer = resolveOneLiner(verb, args);
|
|
217
|
+
if (`error` in answer) {
|
|
218
|
+
throw new Error(answer.error);
|
|
219
|
+
}
|
|
220
|
+
return answer;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Whether the table can express this verb with these arguments. */
|
|
224
|
+
export const oneLinerFits = (verb: OneLinerVerb, args: readonly string[]): boolean => !(`error` in resolveOneLiner(verb, args));
|
|
225
|
+
|
|
226
|
+
/** `https://intentic.dev/<verb>/<arg>…`. Throws when an argument does not fit; check with `oneLinerFits` first. */
|
|
227
|
+
export const oneLinerUrl = (verb: OneLinerVerb, args: readonly string[] = []): string => {
|
|
228
|
+
const normalized = args.map((value, index) => {
|
|
229
|
+
const which = [...VERBS[verb].required, ...VERBS[verb].optional][index];
|
|
230
|
+
return which?.normalize === undefined ? value : which.normalize(value);
|
|
231
|
+
});
|
|
232
|
+
resolved(verb, normalized);
|
|
233
|
+
return [PLATFORM_SITE_ORIGIN, verb, ...normalized].join(`/`);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** The published one-liner: `curl -fsSL <url> | sh` or `irm <url> | iex`. */
|
|
237
|
+
export const oneLinerCommand = (verb: OneLinerVerb, args: readonly string[], shell: OneLinerShell): string => {
|
|
238
|
+
const url = oneLinerUrl(verb, args);
|
|
239
|
+
return shell === `ps1` ? `irm ${url} | iex` : `curl -fsSL ${url} | sh`;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const quote = (value: string): string => `'${value}'`;
|
|
243
|
+
|
|
244
|
+
/** PowerShell parameter tokens: `-Slug 'x' -Rollback`. */
|
|
245
|
+
export const ps1ParamsText = (params: readonly Ps1Param[]): string =>
|
|
246
|
+
params.map((param) => (param.value === undefined ? `-${param.name}` : `-${param.name} ${quote(param.value)}`)).join(` `);
|
|
247
|
+
|
|
248
|
+
/** sh positional args, each single-quoted. */
|
|
249
|
+
export const shArgsText = (args: readonly string[]): string => args.map(quote).join(` `);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The same action run from a checkout of the repository instead of fetched: `sh _site/site/public/scripts/<file> <args>`
|
|
253
|
+
* or `& ./_site/site/public/scripts/<file>.ps1 <args>`. For developers testing the working tree's scripts.
|
|
254
|
+
*/
|
|
255
|
+
export const oneLinerByPath = (verb: OneLinerVerb, args: readonly string[], shell: OneLinerShell): string => {
|
|
256
|
+
const answer = resolved(verb, args);
|
|
257
|
+
if (shell === `ps1`) {
|
|
258
|
+
const params = ps1ParamsText(answer.ps1Params);
|
|
259
|
+
return `& ./${INSTALL_SCRIPTS_DIR}/${answer.files.ps1}${params === `` ? `` : ` ${params}`}`;
|
|
260
|
+
}
|
|
261
|
+
const env = answer.shEnv.map(([name, value]) => ` ${name}=${quote(value)}`).join(``);
|
|
262
|
+
const positional = shArgsText(answer.shArgs);
|
|
263
|
+
return `${env === `` ? `` : `env${env} `}sh ${INSTALL_SCRIPTS_DIR}/${answer.files.sh}${positional === `` ? `` : ` ${positional}`}`;
|
|
264
|
+
};
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// A PROFILE is one named answer to "who is arriving", decided at intentic.dev and carried to the app on the link they
|
|
2
|
+
// click. It has three halves and they land in three different stores, because they have three different lifetimes: the
|
|
3
|
+
// look and the audience are facts about a browser (localStorage, seeded before first paint), and the sandbox defaults
|
|
4
|
+
// are facts about a sandbox (a definition seed applied once, at its first boot).
|
|
5
|
+
//
|
|
6
|
+
// Here so the site, the app's pre-paint script and the platform cannot drift on the names; every value below is
|
|
7
|
+
// spelled somewhere that cannot import it (an inline <script>, a TOML file), and each of those places is pinned by a
|
|
8
|
+
// test that reads this module.
|
|
9
|
+
|
|
10
|
+
/** Names the profile on a link from the site to the app. */
|
|
11
|
+
export const PROFILE_PARAM = "profile";
|
|
12
|
+
|
|
13
|
+
/** Where the app remembers which profile brought this browser; read long after the link that carried it is gone. */
|
|
14
|
+
export const PROFILE_STORAGE_KEY = "ui-profile";
|
|
15
|
+
|
|
16
|
+
// THE COOKIE IS THE SECOND WAY A PROFILE REACHES THE APP, and the only one that survives an installer. The site
|
|
17
|
+
// writes it on the registrable domain both origins share (sharedCookieDomain), so app.intentic.dev reads it too: a
|
|
18
|
+
// reader who took the desktop app from intentic.dev/desk never clicked a link into the app, and the sign-in page the
|
|
19
|
+
// app opens in their browser is the first thing on the app's origin they meet. `desk` means the desk profile; any
|
|
20
|
+
// other value is the site's way out, which is the default profile; absent is no opinion.
|
|
21
|
+
/** The site's edition cookie, readable by the app. */
|
|
22
|
+
export const PROFILE_COOKIE = "variant";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The domain a cookie must be set on for both origins to read it, or undefined when a host-only cookie already does
|
|
26
|
+
* (the same host, or hosts with no registrable domain to share: localhost, an IP).
|
|
27
|
+
*/
|
|
28
|
+
export const sharedCookieDomain = (originA: string, originB: string): string | undefined => {
|
|
29
|
+
const a = new URL(originA).hostname.split(".");
|
|
30
|
+
const b = new URL(originB).hostname.split(".");
|
|
31
|
+
if (a.join(".") === b.join(".")) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const shared: string[] = [];
|
|
35
|
+
while (a.length > 0 && b.length > 0 && a.at(-1) === b.at(-1)) {
|
|
36
|
+
shared.unshift(a.pop() as string);
|
|
37
|
+
b.pop();
|
|
38
|
+
}
|
|
39
|
+
// Two labels is the shortest registrable domain; an all-digit label means an IP, which no domain cookie covers.
|
|
40
|
+
return shared.length >= 2 && shared.every((label) => !/^\d+$/u.test(label)) ? shared.join(".") : undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Every profile there is, as a tuple so a schema can take it and the type below cannot drift from the list. */
|
|
44
|
+
export const PROFILE_IDS = ["default", "desk"] as const;
|
|
45
|
+
|
|
46
|
+
export type Profile = (typeof PROFILE_IDS)[number];
|
|
47
|
+
|
|
48
|
+
// `system` is the stored spelling of "nobody has chosen": the scheme follows `prefers-color-scheme` and the skin
|
|
49
|
+
// follows the scheme. It is a value rather than an absence so a profile can hand a browser back to it.
|
|
50
|
+
export type ProfileScheme = "system" | "light" | "dark";
|
|
51
|
+
export type ProfileSkin = "system" | "none" | "sanctum";
|
|
52
|
+
export type ProfileAudience = "developer" | "maker";
|
|
53
|
+
|
|
54
|
+
/** The localStorage keys a profile seeds, each owned by the composable named beside it. */
|
|
55
|
+
export const PROFILE_KEYS = {
|
|
56
|
+
// _editor/ui/src/composables/useTheme.ts
|
|
57
|
+
scheme: "ui-color-scheme",
|
|
58
|
+
// _editor/web/src/skins/useSkin.ts
|
|
59
|
+
skin: "ui-skin",
|
|
60
|
+
// _editor/web/src/app/useAudience.ts
|
|
61
|
+
audience: "ui-audience",
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export interface ProfileLook {
|
|
65
|
+
readonly scheme: ProfileScheme;
|
|
66
|
+
readonly skin: ProfileSkin;
|
|
67
|
+
// Absent means the profile does not answer it: AudienceAsk still asks, which is what `default` wants.
|
|
68
|
+
readonly audience?: ProfileAudience;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `default` is listed, not implied, because it is also the way back: a profile may overwrite what the last profile
|
|
72
|
+
// wrote, so `?profile=default` undoes a desk link for anyone who has not since chosen for themselves.
|
|
73
|
+
export const PROFILES: Record<Profile, ProfileLook> = {
|
|
74
|
+
default: { scheme: "system", skin: "system" },
|
|
75
|
+
desk: { scheme: "light", skin: "none", audience: "maker" },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_PROFILE: Profile = "default";
|
|
79
|
+
|
|
80
|
+
export const isProfile = (value: unknown): value is Profile => PROFILE_IDS.includes(value as Profile);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Provider brand marks, 24×24 paths meant for fill="currentColor"; shared by the chat/fleet UI and the site's cost band
|
|
2
|
+
// so neither drifts from the other. Every native provider has a row here, enforced by ProviderBrand typing a spec's
|
|
3
|
+
// `brand` field.
|
|
4
|
+
export const PROVIDER_BRAND_PATHS = {
|
|
5
|
+
claude: `m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z`,
|
|
6
|
+
codex: `M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z`,
|
|
7
|
+
grok: `M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815`,
|
|
8
|
+
// Placeholder crescent for Kimi; swap the path here and both surfaces move together.
|
|
9
|
+
kimi: `M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z`,
|
|
10
|
+
gemini: `M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81`,
|
|
11
|
+
// Two subpaths (outline, face); both wind as Simple Icons draws them so nonzero fill renders the notch.
|
|
12
|
+
cursor: `M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23`,
|
|
13
|
+
// Devin's three-hexagon mark from app.devin.ai's own logo (48×48), halved onto this 24×24 grid; one nonzero subpath.
|
|
14
|
+
devin: `M9.2924 10.0645L9.2924 10.0661C9.6314 9.8704 10.0167 9.767 10.4068 9.767L10.4355 9.767L10.4761 9.7678C10.5031 9.7685 10.5294 9.7693 10.5565 9.7717L10.5764 9.7733C10.898 9.7972 11.2036 9.8879 11.4886 10.0446C11.5173 10.0605 11.5451 10.0764 11.573 10.0931C11.5945 10.1067 11.616 10.1202 11.6383 10.1345L11.659 10.148C11.8349 10.2666 11.9933 10.4106 12.1334 10.5792C12.1525 10.6023 12.1708 10.6253 12.1931 10.6548L12.2042 10.6691C12.2186 10.6882 12.2328 10.7081 12.2464 10.728L12.2711 10.7638C12.2822 10.7805 12.2926 10.7972 12.3029 10.8139C12.3117 10.8282 12.3204 10.8425 12.3292 10.8569L12.3443 10.8823C12.5401 11.2204 12.6436 11.607 12.6436 11.9985L12.6428 11.9985C12.6428 12.3907 12.5394 12.7765 12.3435 13.1146L12.3093 13.1727C12.295 13.1958 12.2806 13.2181 12.2663 13.2403L12.2552 13.2571C12.0737 13.5236 11.8428 13.7423 11.5634 13.911C11.5356 13.9277 11.5077 13.9436 11.479 13.9595C11.4568 13.9723 11.4337 13.9834 11.4106 13.9953L11.3883 14.0065C11.1973 14.0995 10.9935 14.1648 10.7777 14.2014C10.7483 14.2061 10.7188 14.2101 10.6893 14.2141L10.6647 14.2173C10.6408 14.2197 10.6169 14.2221 10.5923 14.2244C10.5779 14.2252 10.5636 14.2268 10.5493 14.2276C10.5294 14.2292 10.5102 14.23 10.4904 14.23C10.4736 14.23 10.457 14.2308 10.4402 14.2316L10.41 14.2316L10.4084 14.2316C10.0175 14.2316 9.6323 14.1282 9.2932 13.9325L7.2267 12.7407L3.0921 15.1306L3.0905 19.8999L7.2251 22.2858L11.3612 19.8991L11.3612 17.5133C11.3612 17.1211 11.4647 16.7352 11.6605 16.3971L11.6948 16.339C11.7091 16.316 11.7234 16.2937 11.7378 16.2714L11.7488 16.2547C11.9304 15.9882 12.1612 15.7694 12.4406 15.6008C12.4685 15.584 12.4964 15.5681 12.525 15.5522C12.5473 15.5403 12.5696 15.5284 12.5935 15.5164L12.6158 15.5053C12.8068 15.4122 13.0106 15.347 13.2263 15.3104C13.2558 15.3056 13.2852 15.3016 13.3147 15.2976L13.3394 15.2945C13.3632 15.2921 13.3871 15.2897 13.4126 15.2873L13.4548 15.2841C13.4747 15.2825 13.4938 15.2817 13.5137 15.2817C13.5304 15.2817 13.5471 15.2809 13.5638 15.2801L13.5941 15.2801L13.5957 15.2801C13.9865 15.2801 14.3718 15.3836 14.7109 15.5793L16.7782 16.771L20.9143 14.3844L20.9127 9.6134L16.7789 7.2268L14.7117 8.4217L14.7101 8.4185C14.3702 8.6142 13.9857 8.7152 13.5925 8.7176L13.5662 8.7176L13.5248 8.7168C13.4978 8.716 13.4715 8.7152 13.4444 8.7128L13.4245 8.7113C13.1029 8.6874 12.7973 8.5967 12.5123 8.44C12.4836 8.4241 12.4558 8.4081 12.4279 8.3914C12.4056 8.3779 12.3841 8.3636 12.3626 8.3501L12.3419 8.3366C12.166 8.218 12.0076 8.074 11.8675 7.9054C11.8484 7.8823 11.8301 7.8592 11.8078 7.8298L11.7967 7.8155C11.7823 7.7964 11.768 7.7765 11.7545 7.7566L11.7297 7.7208C11.7187 7.7041 11.7083 7.6874 11.698 7.6707C11.6892 7.6564 11.6796 7.642 11.6709 7.6269L11.6566 7.6023C11.4607 7.2641 11.3573 6.8775 11.3573 6.4861L11.3565 6.4861L11.3565 4.101L7.2418 1.7255L7.2219 1.7143L3.0873 4.1042L3.0857 8.8735L7.2203 11.2594L9.2884 10.0661L9.2924 10.0645Z`,
|
|
15
|
+
// Three subpaths (sweep, two counters), wound as Simple Icons draws them so nonzero fill cuts the counters out.
|
|
16
|
+
meta: `M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z`,
|
|
17
|
+
// Three non-overlapping subpaths; source declares even-odd fill, honoured below rather than assumed.
|
|
18
|
+
zai: `M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z`,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type ProviderBrand = keyof typeof PROVIDER_BRAND_PATHS;
|
|
22
|
+
|
|
23
|
+
// Marks whose source draws them even-odd (grok self-intersects; zai's source declares it); everything else is nonzero.
|
|
24
|
+
// A set, so a new even-odd mark is one place to add, not a second `=== grok` to remember.
|
|
25
|
+
const EVENODD_BRANDS: ReadonlySet<ProviderBrand> = new Set([`grok`, `zai`]);
|
|
26
|
+
export const providerFillRule = (brand: ProviderBrand): "evenodd" | undefined => (EVENODD_BRANDS.has(brand) ? `evenodd` : undefined);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The daemon's turn preamble as a reader outside the sandbox sees it. The daemon puts notes (the project map, the checks
|
|
2
|
+
// note, retrieved context…) in front of what the user typed, and they end at the FIRST separator; the notes and the
|
|
3
|
+
// exact header list stay in the sandbox (agent/prompt/turn-preamble.ts). Session recall reads provider transcripts
|
|
4
|
+
// written that way and cannot depend on the sandbox, so it recognises a preamble by its opening and a drift test there
|
|
5
|
+
// holds every header the daemon parses to this recogniser.
|
|
6
|
+
|
|
7
|
+
export const TURN_PREAMBLE_SEPARATOR = "\n\n---\n\n";
|
|
8
|
+
|
|
9
|
+
// The two notes that open without a markdown heading: the dependency notices. Every other note opens "## ".
|
|
10
|
+
export const SETUP_NOTICE_HEADER =
|
|
11
|
+
"Dependencies are NOT installed for the following projects, so their type-checks, linters and tests cannot work yet";
|
|
12
|
+
export const STALE_NOTICE_HEADER = "Some dependencies declared under /work are not installed";
|
|
13
|
+
|
|
14
|
+
// Wider than the daemon's own list on purpose: transcripts keep notes the daemon has since retired ("## Your branch
|
|
15
|
+
// moved onto newer main"), and those must come off too.
|
|
16
|
+
export const opensWithInjectedNote = (text: string): boolean =>
|
|
17
|
+
text.startsWith("## ") || text.startsWith(SETUP_NOTICE_HEADER) || text.startsWith(STALE_NOTICE_HEADER);
|
|
18
|
+
|
|
19
|
+
// The user's own words with the preamble taken off: cut at the FIRST separator, as the daemon's own parser cuts, and
|
|
20
|
+
// only when the text opens like a note. A note-like opening with no separator, or any other opening, leaves the text
|
|
21
|
+
// whole rather than cut at a guess.
|
|
22
|
+
export const stripInjectedPreamble = (text: string): string => {
|
|
23
|
+
if (!opensWithInjectedNote(text)) {
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
const separator = text.indexOf(TURN_PREAMBLE_SEPARATOR);
|
|
27
|
+
return separator === -1 ? text : text.slice(separator + TURN_PREAMBLE_SEPARATOR.length);
|
|
28
|
+
};
|