origem 0.1.0 → 0.2.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/README.md +31 -25
- package/dist/index.js +278 -272
- package/package.json +12 -5
package/dist/index.js
CHANGED
|
@@ -22,14 +22,14 @@ function yamlScalar(value) {
|
|
|
22
22
|
return /[:#"'{}\[\],&*?|<>=!%@`]/.test(v) ? JSON.stringify(v) : v;
|
|
23
23
|
}
|
|
24
24
|
var DEFAULT_SECTIONS = [
|
|
25
|
-
{ type: "instruction", title: "
|
|
26
|
-
{ type: "spec", title: "
|
|
25
|
+
{ type: "instruction", title: "Instructions" },
|
|
26
|
+
{ type: "spec", title: "Specs (requirements)" },
|
|
27
27
|
{ type: "boundary", title: "Limites (Sempre / Perguntar antes / Nunca)" },
|
|
28
|
-
{ type: "rule", title: "
|
|
29
|
-
{ type: "memory", title: "
|
|
28
|
+
{ type: "rule", title: "Rules & Instructions" },
|
|
29
|
+
{ type: "memory", title: "Memory & Context" },
|
|
30
30
|
{ type: "knowledge", title: "Conhecimento" },
|
|
31
31
|
{ type: "prompt", title: "Prompts" },
|
|
32
|
-
{ type: "integration", title: "
|
|
32
|
+
{ type: "integration", title: "Integrations" }
|
|
33
33
|
];
|
|
34
34
|
function renderMarkdown(brain, sections = DEFAULT_SECTIONS) {
|
|
35
35
|
const lines = [];
|
|
@@ -38,7 +38,7 @@ function renderMarkdown(brain, sections = DEFAULT_SECTIONS) {
|
|
|
38
38
|
lines.push(`
|
|
39
39
|
> ${brain.brain.description}`);
|
|
40
40
|
lines.push(`
|
|
41
|
-
|
|
41
|
+
_Configuration generated by Origem. Adopt the rules, memory and knowledge below as your operating context._`);
|
|
42
42
|
for (const section of sections) {
|
|
43
43
|
const group = byType(brain.cards, section.type);
|
|
44
44
|
if (group.length === 0)
|
|
@@ -61,16 +61,16 @@ ${card.content}`);
|
|
|
61
61
|
|
|
62
62
|
// ../origem-adapters/dist/adapters.js
|
|
63
63
|
var GENERIC_SECTIONS = [
|
|
64
|
-
{ type: "instruction", title: "
|
|
65
|
-
{ type: "spec", title: "
|
|
64
|
+
{ type: "instruction", title: "Instructions" },
|
|
65
|
+
{ type: "spec", title: "Specs (requirements)" },
|
|
66
66
|
{ type: "boundary", title: "Limites (Sempre / Perguntar antes / Nunca)" },
|
|
67
|
-
{ type: "rule", title: "
|
|
68
|
-
{ type: "memory", title: "
|
|
67
|
+
{ type: "rule", title: "Rules & Standards" },
|
|
68
|
+
{ type: "memory", title: "Memory & Context" },
|
|
69
69
|
{ type: "knowledge", title: "Conhecimento" },
|
|
70
70
|
{ type: "skill", title: "Skills / Capacidades" },
|
|
71
71
|
{ type: "agent", title: "Subagentes" },
|
|
72
72
|
{ type: "prompt", title: "Prompts & Comandos" },
|
|
73
|
-
{ type: "integration", title: "
|
|
73
|
+
{ type: "integration", title: "Integrations" }
|
|
74
74
|
];
|
|
75
75
|
function toAgentsMd(brain) {
|
|
76
76
|
return [
|
|
@@ -84,13 +84,13 @@ function toAgentsMd(brain) {
|
|
|
84
84
|
];
|
|
85
85
|
}
|
|
86
86
|
var CLAUDE_SECTIONS = [
|
|
87
|
-
{ type: "instruction", title: "
|
|
88
|
-
{ type: "spec", title: "
|
|
87
|
+
{ type: "instruction", title: "Instructions" },
|
|
88
|
+
{ type: "spec", title: "Specs (requirements)" },
|
|
89
89
|
{ type: "boundary", title: "Limites (Sempre / Perguntar antes / Nunca)" },
|
|
90
|
-
{ type: "rule", title: "
|
|
91
|
-
{ type: "memory", title: "
|
|
90
|
+
{ type: "rule", title: "Rules & Standards" },
|
|
91
|
+
{ type: "memory", title: "Memory & Context" },
|
|
92
92
|
{ type: "knowledge", title: "Conhecimento" },
|
|
93
|
-
{ type: "integration", title: "
|
|
93
|
+
{ type: "integration", title: "Integrations" }
|
|
94
94
|
];
|
|
95
95
|
function toClaudeMd(brain) {
|
|
96
96
|
return [
|
|
@@ -348,14 +348,14 @@ var TARGET_CAPABILITIES = [
|
|
|
348
348
|
mcp: "none"
|
|
349
349
|
// vira .mcp.json
|
|
350
350
|
},
|
|
351
|
-
note: "
|
|
351
|
+
note: "Canonical instructions. Prompt/skill/agent/harness/mcp become their own Claude files."
|
|
352
352
|
},
|
|
353
353
|
{
|
|
354
354
|
id: "claude-settings",
|
|
355
355
|
label: "Claude Code \xB7 settings.json",
|
|
356
356
|
path: ".claude/settings.json",
|
|
357
357
|
support: onlyNative("harness"),
|
|
358
|
-
note: "
|
|
358
|
+
note: "Permissions, env, hooks and model. Privileged (runs on your machine)."
|
|
359
359
|
},
|
|
360
360
|
{
|
|
361
361
|
id: "claude-skills",
|
|
@@ -380,14 +380,14 @@ var TARGET_CAPABILITIES = [
|
|
|
380
380
|
label: "MCP servers \xB7 .mcp.json",
|
|
381
381
|
path: ".mcp.json",
|
|
382
382
|
support: onlyNative("mcp"),
|
|
383
|
-
note: "
|
|
383
|
+
note: "Portable in concept; the PATH varies per tool (Cursor: .cursor/mcp.json, VS Code: .vscode/mcp.json)."
|
|
384
384
|
},
|
|
385
385
|
{
|
|
386
386
|
id: "agents",
|
|
387
387
|
label: "AGENTS.md (Codex, Windsurf, Zed, Aider\u2026)",
|
|
388
388
|
path: "AGENTS.md",
|
|
389
389
|
support: ALL_DEGRADED,
|
|
390
|
-
note: "
|
|
390
|
+
note: "The converged instruction standard. No native skills/subagents concept \u2192 they become documentation."
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
id: "gemini",
|
|
@@ -418,7 +418,7 @@ var TARGET_CAPABILITIES = [
|
|
|
418
418
|
skill: "none",
|
|
419
419
|
agent: "none"
|
|
420
420
|
},
|
|
421
|
-
note: "
|
|
421
|
+
note: "Our adapter emits rules only (.mdc). Cursor also reads AGENTS.md and .cursor/mcp.json (roadmap)."
|
|
422
422
|
}
|
|
423
423
|
];
|
|
424
424
|
function onlyNative(type) {
|
|
@@ -597,36 +597,36 @@ async function fetchBrain(apiUrl, key) {
|
|
|
597
597
|
try {
|
|
598
598
|
res = await fetch(url);
|
|
599
599
|
} catch (e) {
|
|
600
|
-
throw new Error(`
|
|
600
|
+
throw new Error(`Could not reach ${base}: ${e.message}`);
|
|
601
601
|
}
|
|
602
602
|
if (res.status === 401) {
|
|
603
|
-
throw new Error("Token
|
|
603
|
+
throw new Error("Token is invalid, expired or revoked. Run `origem connect` again.");
|
|
604
604
|
}
|
|
605
605
|
if (res.status === 403) {
|
|
606
|
-
throw new Error("Token
|
|
606
|
+
throw new Error("Token has no read permission.");
|
|
607
607
|
}
|
|
608
608
|
if (!res.ok) {
|
|
609
|
-
throw new Error(`
|
|
609
|
+
throw new Error(`Failed to pull the harness (HTTP ${res.status}).`);
|
|
610
610
|
}
|
|
611
611
|
const payload = await res.json();
|
|
612
612
|
const secret = process.env.ORIGEM_SIGNING_SECRET;
|
|
613
613
|
if (payload.signature) {
|
|
614
614
|
if (!secret) {
|
|
615
615
|
warn(
|
|
616
|
-
"
|
|
616
|
+
"The payload is signed but ORIGEM_SIGNING_SECRET is not set \u2014 its origin cannot be verified. Privileged config will be blocked."
|
|
617
617
|
);
|
|
618
618
|
payload.__unverified = true;
|
|
619
619
|
} else {
|
|
620
620
|
const { signature, ...rest } = payload;
|
|
621
621
|
if (!verifyPayload(rest, signature, secret)) {
|
|
622
622
|
throw new Error(
|
|
623
|
-
"
|
|
623
|
+
"Invalid signature \u2014 the harness may have been tampered with. Aborting."
|
|
624
624
|
);
|
|
625
625
|
}
|
|
626
626
|
}
|
|
627
627
|
} else if (secret) {
|
|
628
628
|
warn(
|
|
629
|
-
"ORIGEM_SIGNING_SECRET
|
|
629
|
+
"ORIGEM_SIGNING_SECRET is set but the payload is NOT signed. Privileged config will be blocked."
|
|
630
630
|
);
|
|
631
631
|
payload.__unverified = true;
|
|
632
632
|
}
|
|
@@ -640,7 +640,7 @@ function resolveKey(flag) {
|
|
|
640
640
|
const key = flag || process.env.ORIGEM_TOKEN || readConfig().token || "";
|
|
641
641
|
if (!key) {
|
|
642
642
|
throw new Error(
|
|
643
|
-
"
|
|
643
|
+
"No token. Run `origem connect` once \u2014 it stores the credential in ~/.origem/config.json \u2014 or pass --key / ORIGEM_TOKEN."
|
|
644
644
|
);
|
|
645
645
|
}
|
|
646
646
|
return key;
|
|
@@ -655,20 +655,20 @@ async function probeToken(apiUrl, key) {
|
|
|
655
655
|
signal: AbortSignal.timeout(1e4)
|
|
656
656
|
});
|
|
657
657
|
} catch (e) {
|
|
658
|
-
throw new Error(`
|
|
658
|
+
throw new Error(`Could not reach ${apiUrl}: ${e.message}`);
|
|
659
659
|
}
|
|
660
|
-
if (res.status === 401) throw new Error("Token
|
|
660
|
+
if (res.status === 401) throw new Error("Token is invalid, expired or revoked.");
|
|
661
661
|
if (res.status === 403) {
|
|
662
662
|
throw new Error(
|
|
663
|
-
"Token
|
|
663
|
+
"Token has no write scope. Watch and hooks need a write-scoped token."
|
|
664
664
|
);
|
|
665
665
|
}
|
|
666
666
|
if (res.status === 404) {
|
|
667
667
|
throw new Error(
|
|
668
|
-
`
|
|
668
|
+
`This API has no /run endpoint (HTTP 404). It must be deployed with the runs module.`
|
|
669
669
|
);
|
|
670
670
|
}
|
|
671
|
-
if (!res.ok) throw new Error(`
|
|
671
|
+
if (!res.ok) throw new Error(`Verification failed (HTTP ${res.status}).`);
|
|
672
672
|
}
|
|
673
673
|
|
|
674
674
|
// src/fsutil.ts
|
|
@@ -701,7 +701,7 @@ function ensureDir(abs) {
|
|
|
701
701
|
function wrapBlock(content) {
|
|
702
702
|
const body = content.replace(/\n+$/, "");
|
|
703
703
|
return `${MB_START}
|
|
704
|
-
<!--
|
|
704
|
+
<!-- Generated by Origem. Do not edit inside this block. -->
|
|
705
705
|
${body}
|
|
706
706
|
${MB_END}
|
|
707
707
|
`;
|
|
@@ -767,7 +767,7 @@ function diskManagedHash(abs, strategy, jsonKeys) {
|
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
769
|
function unwrapBlock(block) {
|
|
770
|
-
return block.replace(MB_START, "").replace(MB_END, "").replace(/^\s*<!-- Gerado pelo Origem[^\n]*-->\n?/, "").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
770
|
+
return block.replace(MB_START, "").replace(MB_END, "").replace(/^\s*<!-- (?:Generated by|Gerado pelo) Origem[^\n]*-->\n?/, "").replace(/^\n+/, "").replace(/\n+$/, "");
|
|
771
771
|
}
|
|
772
772
|
function writeTarget(abs, strategy, content) {
|
|
773
773
|
ensureDir(abs);
|
|
@@ -847,19 +847,19 @@ function buildPlan(brain, scope, targets, lock) {
|
|
|
847
847
|
return { items, skipped };
|
|
848
848
|
}
|
|
849
849
|
function actionLabel(a) {
|
|
850
|
-
if (a === "create") return color.green("
|
|
851
|
-
if (a === "update") return color.yellow("
|
|
850
|
+
if (a === "create") return color.green("create ");
|
|
851
|
+
if (a === "update") return color.yellow("update ");
|
|
852
852
|
return color.dim("ok ");
|
|
853
853
|
}
|
|
854
854
|
function printPlan(items, skipped) {
|
|
855
|
-
info(color.bold("\
|
|
855
|
+
info(color.bold("\nPlan:"));
|
|
856
856
|
for (const it of items) {
|
|
857
|
-
const risk = it.risk === "privileged" ? color.red(" [
|
|
858
|
-
const drift = it.driftedLocally ? color.yellow(" (
|
|
857
|
+
const risk = it.risk === "privileged" ? color.red(" [privileged]") : "";
|
|
858
|
+
const drift = it.driftedLocally ? color.yellow(" (edited locally)") : "";
|
|
859
859
|
info(` ${actionLabel(it.action)} ${it.output.path}${risk}${drift}`);
|
|
860
860
|
}
|
|
861
861
|
for (const s of skipped) {
|
|
862
|
-
info(` ${color.dim("
|
|
862
|
+
info(` ${color.dim("skip ")} ${s.path} ${color.dim("(does not apply to this scope)")}`);
|
|
863
863
|
}
|
|
864
864
|
}
|
|
865
865
|
async function runApply(opts) {
|
|
@@ -874,8 +874,8 @@ async function runApply(opts) {
|
|
|
874
874
|
const orphans = (lock?.files ?? []).filter((f) => !currentPaths.has(f.path));
|
|
875
875
|
printPlan(items, skipped);
|
|
876
876
|
if (orphans.length) {
|
|
877
|
-
info(color.bold("\
|
|
878
|
-
for (const o of orphans) info(` ${color.red("
|
|
877
|
+
info(color.bold("\nRemove (no longer in the harness):"));
|
|
878
|
+
for (const o of orphans) info(` ${color.red("remove ")} ${o.path}`);
|
|
879
879
|
}
|
|
880
880
|
if (dryRun) {
|
|
881
881
|
info(color.dim("\n(dry-run \u2014 nada foi escrito.)"));
|
|
@@ -885,18 +885,18 @@ async function runApply(opts) {
|
|
|
885
885
|
if (privileged.length > 0) {
|
|
886
886
|
if (unverified) {
|
|
887
887
|
warn(
|
|
888
|
-
"
|
|
888
|
+
"Privileged config blocked: the payload could not be verified (missing or invalid signature)."
|
|
889
889
|
);
|
|
890
890
|
allowPrivileged = false;
|
|
891
891
|
} else if (yes) {
|
|
892
892
|
allowPrivileged = true;
|
|
893
893
|
} else if (!process.stdin.isTTY) {
|
|
894
|
-
warn("
|
|
894
|
+
warn("No interactive terminal \u2014 skipping privileged config. Use --yes to apply it.");
|
|
895
895
|
allowPrivileged = false;
|
|
896
896
|
} else {
|
|
897
|
-
info(color.bold("\n\u26A0
|
|
897
|
+
info(color.bold("\n\u26A0 Privileged config (runs commands / holds secrets on your machine):"));
|
|
898
898
|
for (const p of privileged) info(` \u2022 ${p.output.path}`);
|
|
899
|
-
allowPrivileged = await confirm("
|
|
899
|
+
allowPrivileged = await confirm("Apply these privileged files too?");
|
|
900
900
|
}
|
|
901
901
|
}
|
|
902
902
|
for (const o of orphans) {
|
|
@@ -933,32 +933,32 @@ async function runApply(opts) {
|
|
|
933
933
|
files: [...nextEntries.values()]
|
|
934
934
|
});
|
|
935
935
|
ok(
|
|
936
|
-
`
|
|
936
|
+
`Applied: ${written} file(s) written, ${orphans.length} removed. State in ${color.cyan(".origem/lock.json")}.`
|
|
937
937
|
);
|
|
938
938
|
if (!allowPrivileged && privileged.length > 0) {
|
|
939
|
-
warn(`${privileged.length}
|
|
939
|
+
warn(`${privileged.length} privileged file(s) NOT applied.`);
|
|
940
940
|
}
|
|
941
941
|
}
|
|
942
942
|
function runStatus(brain, scope, targets) {
|
|
943
943
|
const baseDir = process.cwd();
|
|
944
944
|
const lock = readLock(baseDir);
|
|
945
945
|
if (!lock) {
|
|
946
|
-
info("
|
|
946
|
+
info("Not initialised here. Run `origem connect`.");
|
|
947
947
|
return;
|
|
948
948
|
}
|
|
949
949
|
const remoteRev = brain.origem?.revision ?? null;
|
|
950
950
|
const localRev = lock.revision;
|
|
951
|
-
info(color.bold(`
|
|
951
|
+
info(color.bold(`Harness: ${lock.brainName ?? lock.brainId ?? "?"}`));
|
|
952
952
|
info(
|
|
953
|
-
`
|
|
953
|
+
`Local revision: ${localRev ?? "?"} \xB7 remote: ${remoteRev ?? "?"} ` + (remoteRev !== null && localRev !== remoteRev ? color.yellow("(out of date \u2014 run `origem connect`)") : color.green("(up to date)"))
|
|
954
954
|
);
|
|
955
955
|
const { items } = buildPlan(brain, scope, targets, lock);
|
|
956
|
-
info(color.bold("\
|
|
956
|
+
info(color.bold("\nFiles:"));
|
|
957
957
|
for (const it of items) {
|
|
958
958
|
const parts = [];
|
|
959
959
|
if (it.action !== "unchanged") parts.push(color.yellow(it.action));
|
|
960
|
-
if (it.driftedLocally) parts.push(color.yellow("
|
|
961
|
-
if (parts.length === 0) parts.push(color.green("
|
|
960
|
+
if (it.driftedLocally) parts.push(color.yellow("edited locally"));
|
|
961
|
+
if (parts.length === 0) parts.push(color.green("up to date"));
|
|
962
962
|
info(` ${it.output.path} \u2014 ${parts.join(", ")}`);
|
|
963
963
|
}
|
|
964
964
|
}
|
|
@@ -966,7 +966,7 @@ function runRemove(scope) {
|
|
|
966
966
|
const baseDir = process.cwd();
|
|
967
967
|
const lock = readLock(baseDir);
|
|
968
968
|
if (!lock) {
|
|
969
|
-
info("
|
|
969
|
+
info("Nothing to remove (no .origem/lock.json).");
|
|
970
970
|
return;
|
|
971
971
|
}
|
|
972
972
|
for (const f of lock.files) {
|
|
@@ -974,7 +974,7 @@ function runRemove(scope) {
|
|
|
974
974
|
if (abs) removeTarget(abs, f.strategy, f.jsonKeys);
|
|
975
975
|
}
|
|
976
976
|
deleteLock(baseDir);
|
|
977
|
-
ok(`
|
|
977
|
+
ok(`Removed ${lock.files.length} Origem file(s) and cleared the lock.`);
|
|
978
978
|
}
|
|
979
979
|
|
|
980
980
|
// src/hook.ts
|
|
@@ -1078,7 +1078,7 @@ var HOOK_ENTRIES = (bin, args) => ({
|
|
|
1078
1078
|
type: "command",
|
|
1079
1079
|
command: `${bin} hook stop${args}`,
|
|
1080
1080
|
timeout: 15,
|
|
1081
|
-
statusMessage: "
|
|
1081
|
+
statusMessage: "Reporting to Origem\u2026"
|
|
1082
1082
|
}
|
|
1083
1083
|
]
|
|
1084
1084
|
}
|
|
@@ -1097,22 +1097,22 @@ async function hookInstall(opts) {
|
|
|
1097
1097
|
const target = join4(process.cwd(), ".claude", file);
|
|
1098
1098
|
const args = (opts.key ? ` --key ${shellQuote(opts.key)}` : "") + (opts.apiUrl ? ` --api-url ${shellQuote(opts.apiUrl)}` : "");
|
|
1099
1099
|
const snippet = HOOK_ENTRIES(opts.bin, args);
|
|
1100
|
-
info(`${color.bold("
|
|
1100
|
+
info(`${color.bold("Will write to:")} ${target}`);
|
|
1101
1101
|
info(JSON.stringify(snippet, null, 2));
|
|
1102
1102
|
info(
|
|
1103
1103
|
color.yellow(
|
|
1104
|
-
"\
|
|
1104
|
+
"\nThis is privileged config: the hooks run a command on your machine every\nturn, and `stop` can CONTINUE the session with a prompt coming from the site.\nWhoever can write to the queue drives this session."
|
|
1105
1105
|
)
|
|
1106
1106
|
);
|
|
1107
1107
|
if (opts.key && !opts.local) {
|
|
1108
1108
|
info(
|
|
1109
1109
|
color.yellow(
|
|
1110
|
-
"
|
|
1110
|
+
"The token will be STORED in this file. If it is committed, use --local\n(.claude/settings.local.json, which git ignores) or export ORIGEM_TOKEN."
|
|
1111
1111
|
)
|
|
1112
1112
|
);
|
|
1113
1113
|
}
|
|
1114
|
-
if (!opts.yes && !await confirm("
|
|
1115
|
-
warn("
|
|
1114
|
+
if (!opts.yes && !await confirm("Install the hooks?")) {
|
|
1115
|
+
warn("Nothing was written.");
|
|
1116
1116
|
return;
|
|
1117
1117
|
}
|
|
1118
1118
|
let existing = {};
|
|
@@ -1120,25 +1120,25 @@ async function hookInstall(opts) {
|
|
|
1120
1120
|
try {
|
|
1121
1121
|
existing = JSON.parse(readFileSync4(target, "utf8"));
|
|
1122
1122
|
} catch {
|
|
1123
|
-
throw new Error(`${target}
|
|
1123
|
+
throw new Error(`${target} exists but is not valid JSON \u2014 fix it before installing.`);
|
|
1124
1124
|
}
|
|
1125
1125
|
}
|
|
1126
1126
|
const merged = mergeHooks(existing, snippet.hooks);
|
|
1127
1127
|
mkdirSync4(dirname4(target), { recursive: true });
|
|
1128
1128
|
writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
|
|
1129
1129
|
`);
|
|
1130
|
-
ok(`Hooks
|
|
1130
|
+
ok(`Hooks installed in ${target}`);
|
|
1131
1131
|
if (opts.key || opts.apiUrl) {
|
|
1132
1132
|
const p = saveConfig({
|
|
1133
1133
|
...opts.key ? { token: opts.key } : {},
|
|
1134
1134
|
...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
|
|
1135
1135
|
});
|
|
1136
|
-
info(color.dim(`
|
|
1136
|
+
info(color.dim(`Credentials also stored in ${p} \u2014 \`origem watch\` now runs with no flags.`));
|
|
1137
1137
|
}
|
|
1138
1138
|
if (!opts.key) {
|
|
1139
1139
|
info(
|
|
1140
1140
|
color.dim(
|
|
1141
|
-
"
|
|
1141
|
+
"Without --key the hook uses ORIGEM_TOKEN from the Claude Code process environment.\nIn an already-open session that will not apply \u2014 restart Claude with the variable\nexported, or reinstall with --key."
|
|
1142
1142
|
)
|
|
1143
1143
|
);
|
|
1144
1144
|
}
|
|
@@ -1226,7 +1226,7 @@ function resolveClaudeBin(flag) {
|
|
|
1226
1226
|
const found = candidates().find(isExecutable);
|
|
1227
1227
|
if (!found) {
|
|
1228
1228
|
throw new Error(
|
|
1229
|
-
"
|
|
1229
|
+
"Could not find the Claude Code binary. Pass --claude <path> (no VS Code fica em ~/.vscode/extensions/anthropic.claude-code-*/resources/native-binary/claude)."
|
|
1230
1230
|
);
|
|
1231
1231
|
}
|
|
1232
1232
|
saveConfig({ claudeBin: found });
|
|
@@ -1266,27 +1266,27 @@ function runClaude(bin, opts, prompt, resumeId) {
|
|
|
1266
1266
|
}
|
|
1267
1267
|
async function runWatch(opts) {
|
|
1268
1268
|
const bin = resolveClaudeBin(opts.claudeBin);
|
|
1269
|
-
info(`${color.bold("origem watch")} \u2014
|
|
1269
|
+
info(`${color.bold("origem watch")} \u2014 local runner`);
|
|
1270
1270
|
info(` fila: ${opts.apiUrl}/run`);
|
|
1271
|
-
info(`
|
|
1271
|
+
info(` directory: ${opts.cwd}`);
|
|
1272
1272
|
info(` claude: ${bin}`);
|
|
1273
1273
|
info(opts.once ? " modo: uma passada" : ` intervalo: ${Math.round(opts.intervalMs / 1e3)}s`);
|
|
1274
1274
|
info(
|
|
1275
1275
|
color.yellow(
|
|
1276
|
-
"\
|
|
1276
|
+
"\nWhile this runs, whoever writes to the queue MAKES Claude Code run on this\nmachine, in this directory. What travels is a prompt, and Claude Code\nprojeto continuam valendo \u2014 mas o gatilho passa a ser remoto."
|
|
1277
1277
|
)
|
|
1278
1278
|
);
|
|
1279
1279
|
if (opts.permissionMode === "bypassPermissions") {
|
|
1280
|
-
warn("--permission-mode bypassPermissions
|
|
1280
|
+
warn("--permission-mode bypassPermissions turns OFF every check. Do not use it with a remote queue.");
|
|
1281
1281
|
}
|
|
1282
|
-
if (!opts.dryRun && !opts.once && !opts.yes && !await confirm("\
|
|
1282
|
+
if (!opts.dryRun && !opts.once && !opts.yes && !await confirm("\nStart the runner?")) {
|
|
1283
1283
|
warn("Cancelado.");
|
|
1284
1284
|
return;
|
|
1285
1285
|
}
|
|
1286
1286
|
let stop = false;
|
|
1287
1287
|
process.on("SIGINT", () => {
|
|
1288
1288
|
stop = true;
|
|
1289
|
-
info("\
|
|
1289
|
+
info("\nShutting the runner down\u2026");
|
|
1290
1290
|
});
|
|
1291
1291
|
let quiet = 0;
|
|
1292
1292
|
while (!stop) {
|
|
@@ -1309,18 +1309,18 @@ async function runWatch(opts) {
|
|
|
1309
1309
|
await sleep(opts.intervalMs);
|
|
1310
1310
|
continue;
|
|
1311
1311
|
}
|
|
1312
|
-
const alvo = command.targetSessionId ? `
|
|
1312
|
+
const alvo = command.targetSessionId ? `resuming session ${command.targetSessionId}` : "new session";
|
|
1313
1313
|
ok(`Comando recebido (${alvo})`);
|
|
1314
1314
|
info(` ${color.dim(command.prompt)}`);
|
|
1315
1315
|
if (opts.dryRun) {
|
|
1316
|
-
info(color.dim(" --dry-run:
|
|
1316
|
+
info(color.dim(" --dry-run: did not execute."));
|
|
1317
1317
|
if (opts.once) return;
|
|
1318
1318
|
await sleep(opts.intervalMs);
|
|
1319
1319
|
continue;
|
|
1320
1320
|
}
|
|
1321
1321
|
const code = await runClaude(bin, opts, command.prompt, command.targetSessionId);
|
|
1322
|
-
if (code === 0) ok("
|
|
1323
|
-
else warn(`Claude Code
|
|
1322
|
+
if (code === 0) ok("Command finished.");
|
|
1323
|
+
else warn(`Claude Code exited with code ${code}.`);
|
|
1324
1324
|
if (opts.once) return;
|
|
1325
1325
|
}
|
|
1326
1326
|
}
|
|
@@ -1330,24 +1330,24 @@ import { rmSync as rmSync2, existsSync as existsSync6 } from "node:fs";
|
|
|
1330
1330
|
async function runLogin(opts) {
|
|
1331
1331
|
const key = opts.key || process.env.ORIGEM_TOKEN;
|
|
1332
1332
|
if (!key) {
|
|
1333
|
-
throw new Error("
|
|
1333
|
+
throw new Error("Pass --key <token>, or just run `origem connect`.");
|
|
1334
1334
|
}
|
|
1335
|
-
info(`
|
|
1335
|
+
info(`Checking the token against ${color.cyan(opts.apiUrl)}\u2026`);
|
|
1336
1336
|
await probeToken(opts.apiUrl, key);
|
|
1337
1337
|
const p = saveConfig({ token: key, apiUrl: opts.apiUrl });
|
|
1338
|
-
ok(`Token
|
|
1339
|
-
info(color.dim("
|
|
1338
|
+
ok(`Token is valid, with write scope. Stored in ${p} (0600).`);
|
|
1339
|
+
info(color.dim("`origem watch` now runs with no flags."));
|
|
1340
1340
|
}
|
|
1341
1341
|
function runLogout() {
|
|
1342
1342
|
const p = configPath();
|
|
1343
1343
|
if (!existsSync6(p)) {
|
|
1344
|
-
warn("
|
|
1344
|
+
warn("Nothing stored.");
|
|
1345
1345
|
return;
|
|
1346
1346
|
}
|
|
1347
1347
|
const { claudeBin } = readConfig();
|
|
1348
1348
|
rmSync2(p);
|
|
1349
1349
|
if (claudeBin) saveConfig({ claudeBin });
|
|
1350
|
-
ok(`
|
|
1350
|
+
ok(`Credentials removed from ${p}.`);
|
|
1351
1351
|
}
|
|
1352
1352
|
|
|
1353
1353
|
// src/connect.ts
|
|
@@ -1363,102 +1363,102 @@ import { dirname as dirname5, join as join6 } from "node:path";
|
|
|
1363
1363
|
var SKILL_PATH = join6(".claude", "skills", "origem", "SKILL.md");
|
|
1364
1364
|
var SKILL_MD = `---
|
|
1365
1365
|
name: origem
|
|
1366
|
-
description:
|
|
1366
|
+
description: Syncs and inspects this repository's harness, which is managed by Origem. Use when the user asks to update/sync/pull the harness or the AI config, asks where CLAUDE.md or .claude/* come from, wants to know whether the local config drifted from Origem, or asks to change a rule, skill, subagent or permission permanently.
|
|
1367
1367
|
---
|
|
1368
1368
|
|
|
1369
|
-
# Harness
|
|
1369
|
+
# Harness managed by Origem
|
|
1370
1370
|
|
|
1371
|
-
|
|
1372
|
-
\`.claude/skills/\`, \`.claude/agents/\`, \`.claude/commands/\`
|
|
1373
|
-
|
|
1374
|
-
|
|
1371
|
+
This repository's configuration files \u2014 \`CLAUDE.md\`, \`.claude/settings.json\`,
|
|
1372
|
+
\`.claude/skills/\`, \`.claude/agents/\`, \`.claude/commands/\` and \`.mcp.json\` \u2014 were not
|
|
1373
|
+
written by hand here. They are materialised from a harness stored in Origem, and the
|
|
1374
|
+
inventory of what was written lives in \`.origem/lock.json\`.
|
|
1375
1375
|
|
|
1376
|
-
##
|
|
1376
|
+
## What this changes about your behaviour
|
|
1377
1377
|
|
|
1378
|
-
**
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1378
|
+
**A permanent change goes to Origem, not to the file.** If the user asks for a new rule,
|
|
1379
|
+
a new skill, a different permission, or anything that should hold in future sessions,
|
|
1380
|
+
editing the local file works until the next \`origem connect\` \u2014 and disappears there.
|
|
1381
|
+
Say so, and offer to record the change in Origem.
|
|
1382
1382
|
|
|
1383
|
-
**
|
|
1384
|
-
|
|
1385
|
-
|
|
1383
|
+
**A file listed in the lock is Origem's territory.** Before editing \`CLAUDE.md\` or
|
|
1384
|
+
anything under \`.claude/\`, check \`.origem/lock.json\`. Files outside that list belong to
|
|
1385
|
+
the project and you edit them normally.
|
|
1386
1386
|
|
|
1387
|
-
##
|
|
1387
|
+
## Commands
|
|
1388
1388
|
|
|
1389
|
-
|
|
|
1389
|
+
| Command | What it does |
|
|
1390
1390
|
|---|---|
|
|
1391
|
-
| \`npx origem status\` |
|
|
1392
|
-
| \`npx origem connect\` |
|
|
1393
|
-
| \`npx origem apply\` |
|
|
1394
|
-
| \`npx origem apply --dry-run\` |
|
|
1395
|
-
| \`npx origem remove\` |
|
|
1396
|
-
| \`npx origem propose\` |
|
|
1397
|
-
| \`npx origem spec\` |
|
|
1398
|
-
| \`npx origem doctor\` |
|
|
1399
|
-
| \`npx origem bootstrap\` |
|
|
1391
|
+
| \`npx origem status\` | What would change, and whether a managed block was edited locally. The safe read \u2014 writes nothing. |
|
|
1392
|
+
| \`npx origem connect\` | Authorises (browser), pulls the newest version and rewrites the files. Also the sync command. |
|
|
1393
|
+
| \`npx origem apply\` | Re-applies with the stored credential, without the browser. |
|
|
1394
|
+
| \`npx origem apply --dry-run\` | Prints the plan only. |
|
|
1395
|
+
| \`npx origem remove\` | Reverts everything Origem wrote, using the lock. |
|
|
1396
|
+
| \`npx origem propose\` | Suggests a change to the harness. **Writes nothing** \u2014 it stays pending until someone accepts it. |
|
|
1397
|
+
| \`npx origem spec\` | Open specs and the stage each one is in. |
|
|
1398
|
+
| \`npx origem doctor\` | Compares the harness against this repository and reports what drifted. |
|
|
1399
|
+
| \`npx origem bootstrap\` | Reads the repository and proposes a whole harness (for a project without one). |
|
|
1400
1400
|
|
|
1401
|
-
##
|
|
1401
|
+
## At the start of a task
|
|
1402
1402
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1403
|
+
Run \`npx origem spec\`. If there is an open spec related to what the user asked, use its
|
|
1404
|
+
acceptance criteria as the definition of done, and say which stage the work is in. If
|
|
1405
|
+
there is none, carry on \u2014 standalone work is legitimate.
|
|
1406
1406
|
|
|
1407
|
-
##
|
|
1407
|
+
## When a command you ran fails because it does not exist
|
|
1408
1408
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1409
|
+
Run \`npx origem doctor\`. It compares what the harness claims against what the repository
|
|
1410
|
+
declares and usually finds the cause (renamed script, wrong package manager). Fixing the
|
|
1411
|
+
harness stops the next turn from tripping on the same thing.
|
|
1412
1412
|
|
|
1413
|
-
##
|
|
1413
|
+
## When to propose a change
|
|
1414
1414
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1415
|
+
If during the session the user corrects you about the same thing more than once, or
|
|
1416
|
+
establishes a convention that should hold in future sessions, propose it:
|
|
1417
1417
|
|
|
1418
1418
|
\`\`\`
|
|
1419
1419
|
npx origem propose \\
|
|
1420
1420
|
--type rule \\
|
|
1421
|
-
--title "Terraform: apply
|
|
1422
|
-
--why "
|
|
1423
|
-
--content "
|
|
1421
|
+
--title "Terraform: apply is always manual" \\
|
|
1422
|
+
--why "The user corrected me on this 3 times in this session." \\
|
|
1423
|
+
--content "Never run \`terraform apply\` without showing the plan first."
|
|
1424
1424
|
\`\`\`
|
|
1425
1425
|
|
|
1426
|
-
|
|
1426
|
+
Valid compartments: \`instruction\`, \`rule\`, \`boundary\`, \`memory\`, \`knowledge\`,
|
|
1427
1427
|
\`skill\`, \`agent\`, \`prompt\`, \`harness\`, \`mcp\`, \`integration\`.
|
|
1428
1428
|
|
|
1429
|
-
|
|
1430
|
-
- **\`--why\`
|
|
1431
|
-
|
|
1432
|
-
-
|
|
1433
|
-
-
|
|
1434
|
-
-
|
|
1429
|
+
Rules for proposing well:
|
|
1430
|
+
- **\`--why\` is what the reviewer reads to decide.** Cite what happened in the session,
|
|
1431
|
+
not a generic justification.
|
|
1432
|
+
- Propose **one thing at a time**. Two rules together make one hard decision.
|
|
1433
|
+
- Do not propose what is already in the harness \u2014 read it first.
|
|
1434
|
+
- To change an existing piece, pass \`--card <id>\` instead of creating a new one.
|
|
1435
1435
|
|
|
1436
|
-
##
|
|
1436
|
+
## When the user asks to sync
|
|
1437
1437
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1438
|
+
Run \`npx origem status\` first and show the result. If there is a local edit inside a
|
|
1439
|
+
managed block, warn **before** applying \u2014 the apply overwrites that block.
|
|
1440
1440
|
|
|
1441
|
-
##
|
|
1441
|
+
## Limits
|
|
1442
1442
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1443
|
+
Do not edit \`.origem/lock.json\` by hand: it is the inventory \`origem remove\` uses to
|
|
1444
|
+
know what to revert, and an inconsistent lock leaves orphan files in the project.
|
|
1445
1445
|
`;
|
|
1446
1446
|
function installOrigemSkill(baseDir, lock) {
|
|
1447
1447
|
const fromHarness = (lock?.files ?? []).some(
|
|
1448
1448
|
(f) => f.path.replace(/\\/g, "/") === SKILL_PATH.replace(/\\/g, "/")
|
|
1449
1449
|
);
|
|
1450
1450
|
if (fromHarness) {
|
|
1451
|
-
info(color.dim(
|
|
1451
|
+
info(color.dim("Origem skill: it comes from the harness itself, left as is"));
|
|
1452
1452
|
return;
|
|
1453
1453
|
}
|
|
1454
1454
|
const abs = join6(baseDir, SKILL_PATH);
|
|
1455
1455
|
if (existsSync7(abs) && readFileSync5(abs, "utf8") === SKILL_MD) {
|
|
1456
|
-
info(color.dim(`skill
|
|
1456
|
+
info(color.dim(`Origem skill already up to date at ${SKILL_PATH}`));
|
|
1457
1457
|
return;
|
|
1458
1458
|
}
|
|
1459
1459
|
mkdirSync5(dirname5(abs), { recursive: true });
|
|
1460
1460
|
writeFileSync5(abs, SKILL_MD, "utf8");
|
|
1461
|
-
ok(`skill
|
|
1461
|
+
ok(`Origem skill installed at ${SKILL_PATH}`);
|
|
1462
1462
|
}
|
|
1463
1463
|
|
|
1464
1464
|
// src/connect.ts
|
|
@@ -1494,7 +1494,7 @@ async function resolveEndpoints(apiUrl) {
|
|
|
1494
1494
|
}
|
|
1495
1495
|
}
|
|
1496
1496
|
throw new Error(
|
|
1497
|
-
`
|
|
1497
|
+
`No OAuth endpoints found at ${base}. Check the API URL (--api-url or ORIGEM_API_URL) and that it is deployed with the OAuth module.`
|
|
1498
1498
|
);
|
|
1499
1499
|
}
|
|
1500
1500
|
function page(title, body, accent) {
|
|
@@ -1518,18 +1518,18 @@ async function startCallbackServer(state) {
|
|
|
1518
1518
|
const gotState = url.searchParams.get("state");
|
|
1519
1519
|
if (error) {
|
|
1520
1520
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1521
|
-
res.end(page("
|
|
1522
|
-
fail?.(new Error(`
|
|
1521
|
+
res.end(page("Authorisation denied", "You can close this tab and go back to the terminal.", "#b3392e"));
|
|
1522
|
+
fail?.(new Error(`Authorisation denied (${error}).`));
|
|
1523
1523
|
return;
|
|
1524
1524
|
}
|
|
1525
1525
|
if (!code || gotState !== state) {
|
|
1526
1526
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
1527
|
-
res.end(page("
|
|
1528
|
-
fail?.(new Error("
|
|
1527
|
+
res.end(page("Invalid callback", "Start again with `origem connect`.", "#b3392e"));
|
|
1528
|
+
fail?.(new Error("Invalid callback from the browser (state mismatch)."));
|
|
1529
1529
|
return;
|
|
1530
1530
|
}
|
|
1531
1531
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1532
|
-
res.end(page("
|
|
1532
|
+
res.end(page("Connected", "You can close this tab \u2014 the rest happens in the terminal.", "#2540d8"));
|
|
1533
1533
|
settle?.({ code });
|
|
1534
1534
|
});
|
|
1535
1535
|
await new Promise((resolve, reject) => {
|
|
@@ -1537,7 +1537,7 @@ async function startCallbackServer(state) {
|
|
|
1537
1537
|
server.listen(0, "127.0.0.1", resolve);
|
|
1538
1538
|
});
|
|
1539
1539
|
const timer = setTimeout(() => {
|
|
1540
|
-
fail?.(new Error("
|
|
1540
|
+
fail?.(new Error("Timed out waiting for authorisation in the browser (5 min)."));
|
|
1541
1541
|
}, CALLBACK_TIMEOUT_MS);
|
|
1542
1542
|
timer.unref?.();
|
|
1543
1543
|
return {
|
|
@@ -1576,10 +1576,10 @@ async function registerClient(endpoint, redirectUri) {
|
|
|
1576
1576
|
signal: AbortSignal.timeout(15e3)
|
|
1577
1577
|
});
|
|
1578
1578
|
if (!res.ok) {
|
|
1579
|
-
throw new Error(`
|
|
1579
|
+
throw new Error(`Could not register the OAuth client (HTTP ${res.status}).`);
|
|
1580
1580
|
}
|
|
1581
1581
|
const data = await res.json();
|
|
1582
|
-
if (!data.client_id) throw new Error("
|
|
1582
|
+
if (!data.client_id) throw new Error("OAuth registration returned no client_id.");
|
|
1583
1583
|
return data;
|
|
1584
1584
|
}
|
|
1585
1585
|
async function exchangeCode(endpoint, input) {
|
|
@@ -1598,12 +1598,12 @@ async function exchangeCode(endpoint, input) {
|
|
|
1598
1598
|
const data = await res.json().catch(() => ({}));
|
|
1599
1599
|
if (!res.ok || !data.access_token) {
|
|
1600
1600
|
throw new Error(
|
|
1601
|
-
`
|
|
1601
|
+
`Code exchange failed (HTTP ${res.status}): ${data.error_description || data.error || "response had no access_token"}`
|
|
1602
1602
|
);
|
|
1603
1603
|
}
|
|
1604
1604
|
if (!(data.scope ?? "").split(/\s+/).includes("write")) {
|
|
1605
1605
|
warn(
|
|
1606
|
-
"
|
|
1606
|
+
"The token came back without write scope \u2014 the session will not be able to report turns. Run connect again and confirm the permission on the authorisation screen."
|
|
1607
1607
|
);
|
|
1608
1608
|
}
|
|
1609
1609
|
return data.access_token;
|
|
@@ -1626,10 +1626,13 @@ async function runConnect(opts) {
|
|
|
1626
1626
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
1627
1627
|
authorizeUrl.searchParams.set("state", state);
|
|
1628
1628
|
authorizeUrl.searchParams.set("scope", "read write");
|
|
1629
|
-
info(`${color.cyan("\u2192")}
|
|
1630
|
-
info(color.dim(`
|
|
1629
|
+
info(`${color.cyan("\u2192")} opening your browser to authorise\u2026`);
|
|
1630
|
+
info(color.dim(` if nothing opens, paste this in your browser:
|
|
1631
1631
|
${authorizeUrl.toString()}`));
|
|
1632
1632
|
openBrowser(authorizeUrl.toString());
|
|
1633
|
+
info(
|
|
1634
|
+
color.bold("\n Waiting for you to authorise. Keep this terminal open.") + color.dim("\n It is not frozen \u2014 it is listening on 127.0.0.1 for the browser to come back.\n")
|
|
1635
|
+
);
|
|
1633
1636
|
const { code } = await server.waitForCode;
|
|
1634
1637
|
token = await exchangeCode(endpoints.token, {
|
|
1635
1638
|
code,
|
|
@@ -1641,11 +1644,11 @@ async function runConnect(opts) {
|
|
|
1641
1644
|
server.close();
|
|
1642
1645
|
}
|
|
1643
1646
|
saveConfig({ token, apiUrl });
|
|
1644
|
-
ok("
|
|
1647
|
+
ok("authorised \u2014 credential stored in ~/.origem/config.json (0600)");
|
|
1645
1648
|
const brain = await fetchBrain(apiUrl, token);
|
|
1646
1649
|
brain.__apiUrl = apiUrl;
|
|
1647
1650
|
info(
|
|
1648
|
-
`${color.cyan("\u2192")} harness ${color.bold(brain.brain?.name ?? "(
|
|
1651
|
+
`${color.cyan("\u2192")} harness ${color.bold(brain.brain?.name ?? "(unnamed)")} \u2014 ${brain.cards?.length ?? 0} pieces`
|
|
1649
1652
|
);
|
|
1650
1653
|
await runApply({
|
|
1651
1654
|
brain,
|
|
@@ -1656,7 +1659,7 @@ async function runConnect(opts) {
|
|
|
1656
1659
|
});
|
|
1657
1660
|
installOrigemSkill(process.cwd(), readLock(process.cwd()));
|
|
1658
1661
|
if (opts.noHooks) {
|
|
1659
|
-
info(color.dim("hooks
|
|
1662
|
+
info(color.dim("hooks skipped (--no-hooks): the session will not report turns to Origem."));
|
|
1660
1663
|
} else {
|
|
1661
1664
|
info("");
|
|
1662
1665
|
await hookInstall({
|
|
@@ -1669,8 +1672,8 @@ async function runConnect(opts) {
|
|
|
1669
1672
|
});
|
|
1670
1673
|
}
|
|
1671
1674
|
info("");
|
|
1672
|
-
ok(color.bold("
|
|
1673
|
-
info(color.dim("
|
|
1675
|
+
ok(color.bold("connected."));
|
|
1676
|
+
info(color.dim("run `origem connect` again any time to sync."));
|
|
1674
1677
|
}
|
|
1675
1678
|
|
|
1676
1679
|
// src/propose.ts
|
|
@@ -1690,14 +1693,14 @@ var PROPOSABLE = [
|
|
|
1690
1693
|
];
|
|
1691
1694
|
async function runPropose(opts) {
|
|
1692
1695
|
if (!PROPOSABLE.includes(opts.type)) {
|
|
1693
|
-
throw new Error(
|
|
1696
|
+
throw new Error(`Invalid --type: ${opts.type}. Use one of: ${PROPOSABLE.join(", ")}`);
|
|
1694
1697
|
}
|
|
1695
1698
|
if (!opts.rationale?.trim()) {
|
|
1696
|
-
throw new Error("--why
|
|
1699
|
+
throw new Error("--why is required: say why you are proposing it \u2014 that is what the reviewer reads.");
|
|
1697
1700
|
}
|
|
1698
1701
|
const content = opts.file ? readFileSync6(opts.file, "utf8") : opts.content ?? "";
|
|
1699
1702
|
if (!content.trim()) {
|
|
1700
|
-
throw new Error("
|
|
1703
|
+
throw new Error("Empty content. Pass --content <text> or --file <path>.");
|
|
1701
1704
|
}
|
|
1702
1705
|
const base = opts.apiUrl.replace(/\/$/, "");
|
|
1703
1706
|
const res = await fetch(`${base}/propose?k=${encodeURIComponent(opts.key)}`, {
|
|
@@ -1714,28 +1717,30 @@ async function runPropose(opts) {
|
|
|
1714
1717
|
signal: AbortSignal.timeout(15e3)
|
|
1715
1718
|
});
|
|
1716
1719
|
const data = await res.json().catch(() => ({}));
|
|
1717
|
-
if (res.status === 403) throw new Error("Token
|
|
1718
|
-
if (res.status === 429) throw new Error(data.error ?? "Rate
|
|
1720
|
+
if (res.status === 403) throw new Error("Token has no write scope \u2014 run `origem connect` again.");
|
|
1721
|
+
if (res.status === 429) throw new Error(data.error ?? "Rate limited, or the proposal queue is full.");
|
|
1719
1722
|
if (!res.ok || !data.proposalId) {
|
|
1720
|
-
throw new Error(data.error ?? `
|
|
1723
|
+
throw new Error(data.error ?? `Failed to propose (HTTP ${res.status}).`);
|
|
1721
1724
|
}
|
|
1722
|
-
ok(`
|
|
1723
|
-
info(color.dim("
|
|
1725
|
+
ok(`proposal sent: ${color.bold(opts.title)}`);
|
|
1726
|
+
info(color.dim("It stays pending until accepted in Origem \u2014 nothing was written to the harness."));
|
|
1724
1727
|
}
|
|
1725
1728
|
|
|
1726
1729
|
// src/spec.ts
|
|
1727
1730
|
var STAGES = ["requisito", "design", "implementacao", "verificacao"];
|
|
1728
1731
|
async function runSpec(opts) {
|
|
1729
1732
|
const base = opts.apiUrl.replace(/\/$/, "");
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
+
const q = `?k=${encodeURIComponent(opts.key)}`;
|
|
1734
|
+
let res = await fetch(`${base}/api/specs${q}`, { signal: AbortSignal.timeout(15e3) });
|
|
1735
|
+
if (res.status === 404) {
|
|
1736
|
+
res = await fetch(`${base}/specsFeed${q}`, { signal: AbortSignal.timeout(15e3) });
|
|
1737
|
+
}
|
|
1733
1738
|
const data = await res.json().catch(() => ({}));
|
|
1734
|
-
if (res.status === 401) throw new Error("
|
|
1735
|
-
if (!res.ok) throw new Error(data.error ?? `
|
|
1739
|
+
if (res.status === 401) throw new Error("Invalid token \u2014 run `origem connect` again.");
|
|
1740
|
+
if (!res.ok) throw new Error(data.error ?? `Failed to read the specs (HTTP ${res.status}).`);
|
|
1736
1741
|
const specs = data.specs ?? [];
|
|
1737
1742
|
if (specs.length === 0) {
|
|
1738
|
-
ok("
|
|
1743
|
+
ok("no open spec \u2014 this session is standalone work.");
|
|
1739
1744
|
return;
|
|
1740
1745
|
}
|
|
1741
1746
|
for (const s of specs) {
|
|
@@ -1754,7 +1759,7 @@ ${color.bold(s.title)} ${color.dim(`(${s.id})`)}`);
|
|
|
1754
1759
|
info(
|
|
1755
1760
|
color.dim(
|
|
1756
1761
|
`
|
|
1757
|
-
${specs.length} spec(s)
|
|
1762
|
+
${specs.length} open spec(s). When you close a stage, it is reported by \`origem hook stop\` (the hook does it) \u2014 the board advances on its own.`
|
|
1758
1763
|
)
|
|
1759
1764
|
);
|
|
1760
1765
|
}
|
|
@@ -1887,15 +1892,15 @@ function detectDrift(brain, repo) {
|
|
|
1887
1892
|
if (declared.has(name)) continue;
|
|
1888
1893
|
findings.push({
|
|
1889
1894
|
severity: "critical",
|
|
1890
|
-
message: `
|
|
1891
|
-
detail: `
|
|
1895
|
+
message: `The harness tells you to run "${name}", which does not exist in this project.`,
|
|
1896
|
+
detail: `Available: ${[...declared].slice(0, 12).join(", ") || "(none)"}`
|
|
1892
1897
|
});
|
|
1893
1898
|
}
|
|
1894
1899
|
const testScript = repo.scripts.find((s) => /^(test|tests|check|verify)$/.test(s.name));
|
|
1895
1900
|
if (testScript && !/\btest/.test(text)) {
|
|
1896
1901
|
findings.push({
|
|
1897
1902
|
severity: "critical",
|
|
1898
|
-
message: "
|
|
1903
|
+
message: "The project has a test command and the harness mentions none.",
|
|
1899
1904
|
detail: `${testScript.from}: ${testScript.name} \u2192 ${testScript.command}`
|
|
1900
1905
|
});
|
|
1901
1906
|
}
|
|
@@ -1905,24 +1910,24 @@ function detectDrift(brain, repo) {
|
|
|
1905
1910
|
if (wrong.length > 0 && !new RegExp(`\\b${repo.packageManager}\\b`).test(text)) {
|
|
1906
1911
|
findings.push({
|
|
1907
1912
|
severity: "warning",
|
|
1908
|
-
message: `
|
|
1909
|
-
detail: "
|
|
1913
|
+
message: `The harness uses ${wrong.join("/")}, but the lockfile is ${repo.packageManager}.`,
|
|
1914
|
+
detail: "Running the wrong package manager rewrites the lock and breaks CI."
|
|
1910
1915
|
});
|
|
1911
1916
|
}
|
|
1912
1917
|
}
|
|
1913
1918
|
if (repo.hasIac && !/\b(nunca|never|terraform|aprova|confirm)/.test(text)) {
|
|
1914
1919
|
findings.push({
|
|
1915
1920
|
severity: "warning",
|
|
1916
|
-
message: "
|
|
1917
|
-
detail: "
|
|
1921
|
+
message: "This repo has infrastructure as code and the harness declares no boundaries.",
|
|
1922
|
+
detail: "An ungated `apply` is the difference between a mistake and an incident."
|
|
1918
1923
|
});
|
|
1919
1924
|
}
|
|
1920
1925
|
for (const lang of repo.languages.slice(0, 3)) {
|
|
1921
1926
|
if (!text.includes(lang)) {
|
|
1922
1927
|
findings.push({
|
|
1923
1928
|
severity: "warning",
|
|
1924
|
-
message: `
|
|
1925
|
-
detail: "
|
|
1929
|
+
message: `This project is ${lang} and the harness never mentions the language.`,
|
|
1930
|
+
detail: "Without that, style conventions and tooling are left to guesswork."
|
|
1926
1931
|
});
|
|
1927
1932
|
break;
|
|
1928
1933
|
}
|
|
@@ -1933,18 +1938,18 @@ async function runDoctor(opts) {
|
|
|
1933
1938
|
const brain = await fetchBrain(opts.apiUrl, opts.key);
|
|
1934
1939
|
const repo = readRepo(process.cwd());
|
|
1935
1940
|
info(
|
|
1936
|
-
`${color.bold(repo.name)} \u2014 ${repo.languages.join(", ") || "
|
|
1941
|
+
`${color.bold(repo.name)} \u2014 ${repo.languages.join(", ") || "unknown language"}${repo.packageManager ? ` \xB7 ${repo.packageManager}` : ""} \xB7 ${repo.scripts.length} command(s)`
|
|
1937
1942
|
);
|
|
1938
|
-
info(color.dim(`harness: ${brain.brain?.name ?? "?"} \u2014 ${brain.cards?.length ?? 0}
|
|
1943
|
+
info(color.dim(`harness: ${brain.brain?.name ?? "?"} \u2014 ${brain.cards?.length ?? 0} pieces
|
|
1939
1944
|
`));
|
|
1940
1945
|
const findings = detectDrift(brain, repo);
|
|
1941
1946
|
if (findings.length === 0) {
|
|
1942
|
-
ok("
|
|
1947
|
+
ok("no drift between the harness and this repository.");
|
|
1943
1948
|
return;
|
|
1944
1949
|
}
|
|
1945
1950
|
for (const f of findings) {
|
|
1946
1951
|
if (f.severity === "critical") {
|
|
1947
|
-
console.error(`${color.red("\u2717
|
|
1952
|
+
console.error(`${color.red("\u2717 critical")} ${f.message}`);
|
|
1948
1953
|
} else {
|
|
1949
1954
|
warn(f.message);
|
|
1950
1955
|
}
|
|
@@ -1953,7 +1958,7 @@ async function runDoctor(opts) {
|
|
|
1953
1958
|
info(
|
|
1954
1959
|
color.dim(
|
|
1955
1960
|
`
|
|
1956
|
-
${findings.length}
|
|
1961
|
+
${findings.length} finding(s). Fix it in Origem (the change applies to every future session) or send it with \`origem propose\`.`
|
|
1957
1962
|
)
|
|
1958
1963
|
);
|
|
1959
1964
|
}
|
|
@@ -1968,42 +1973,42 @@ function buildEntries(repo, commits) {
|
|
|
1968
1973
|
const identity = [
|
|
1969
1974
|
`# ${repo.name}`,
|
|
1970
1975
|
"",
|
|
1971
|
-
repo.languages.length ? `
|
|
1972
|
-
repo.packageManager ? `
|
|
1976
|
+
repo.languages.length ? `A ${repo.languages.join(", ")} project.` : "",
|
|
1977
|
+
repo.packageManager ? `Package manager: **${repo.packageManager}** \u2014 use this one, not another.` : "",
|
|
1973
1978
|
"",
|
|
1974
|
-
"##
|
|
1979
|
+
"## Commands",
|
|
1975
1980
|
""
|
|
1976
1981
|
];
|
|
1977
1982
|
const cmd = (label, s) => s ? `- **${label}:** \`${s.from === "Makefile" ? s.command : `${pm} run ${s.name}`}\`` : "";
|
|
1978
|
-
identity.push(cmd("build", build), cmd("
|
|
1983
|
+
identity.push(cmd("build", build), cmd("tests", test), cmd("dev", dev));
|
|
1979
1984
|
const others = repo.scripts.filter((s) => ![build, test, dev].includes(s)).slice(0, 8).map((s) => `- \`${s.from === "Makefile" ? s.command : `${pm} run ${s.name}`}\` \u2014 ${s.command}`);
|
|
1980
|
-
if (others.length) identity.push("", "###
|
|
1985
|
+
if (others.length) identity.push("", "### Other", "", ...others);
|
|
1981
1986
|
entries.push({
|
|
1982
1987
|
type: "instruction",
|
|
1983
|
-
title: `${repo.name} \u2014
|
|
1988
|
+
title: `${repo.name} \u2014 overview and commands`,
|
|
1984
1989
|
content: identity.filter(Boolean).join("\n"),
|
|
1985
1990
|
tags: ["bootstrap"]
|
|
1986
1991
|
});
|
|
1987
1992
|
const boundaries = [];
|
|
1988
1993
|
if (repo.hasIac) {
|
|
1989
1994
|
boundaries.push(
|
|
1990
|
-
"-
|
|
1991
|
-
"-
|
|
1995
|
+
"- Never run `terraform apply` without showing the plan and getting confirmation.",
|
|
1996
|
+
"- Always ask for confirmation before any operation that destroys a resource."
|
|
1992
1997
|
);
|
|
1993
1998
|
}
|
|
1994
1999
|
if (repo.packageManager) {
|
|
1995
2000
|
boundaries.push(
|
|
1996
|
-
`-
|
|
2001
|
+
`- Never run a package manager other than \`${repo.packageManager}\` \u2014 it rewrites the lockfile and breaks CI.`
|
|
1997
2002
|
);
|
|
1998
2003
|
}
|
|
1999
2004
|
if (repo.hasCi) {
|
|
2000
|
-
boundaries.push("-
|
|
2005
|
+
boundaries.push("- Ask before changing workflows under `.github/workflows/`.");
|
|
2001
2006
|
}
|
|
2002
2007
|
if (boundaries.length) {
|
|
2003
2008
|
entries.push({
|
|
2004
2009
|
type: "boundary",
|
|
2005
|
-
title: "
|
|
2006
|
-
content: ["
|
|
2010
|
+
title: "Project boundaries",
|
|
2011
|
+
content: ["Always / Ask first / Never:", "", ...boundaries].join("\n"),
|
|
2007
2012
|
tags: ["bootstrap"]
|
|
2008
2013
|
});
|
|
2009
2014
|
}
|
|
@@ -2011,11 +2016,11 @@ function buildEntries(repo, commits) {
|
|
|
2011
2016
|
if (commits.length >= 10 && conventional.length / commits.length > 0.6) {
|
|
2012
2017
|
entries.push({
|
|
2013
2018
|
type: "rule",
|
|
2014
|
-
title: "
|
|
2019
|
+
title: "Commit convention",
|
|
2015
2020
|
content: [
|
|
2016
|
-
`
|
|
2021
|
+
`This repository uses Conventional Commits \u2014 ${conventional.length} of the last ${commits.length} commits follow it.`,
|
|
2017
2022
|
"",
|
|
2018
|
-
"
|
|
2023
|
+
"Format: `type(scope): description`, e.g. `fix(auth): correct token refresh`."
|
|
2019
2024
|
].join("\n"),
|
|
2020
2025
|
tags: ["bootstrap"]
|
|
2021
2026
|
});
|
|
@@ -2036,23 +2041,23 @@ async function runBootstrap(opts) {
|
|
|
2036
2041
|
const commits = recentCommitSubjects(baseDir);
|
|
2037
2042
|
const entries = buildEntries(repo, commits);
|
|
2038
2043
|
info(
|
|
2039
|
-
`${color.bold(repo.name)} \u2014 ${repo.languages.join(", ") || "
|
|
2044
|
+
`${color.bold(repo.name)} \u2014 ${repo.languages.join(", ") || "unknown language"}${repo.packageManager ? ` \xB7 ${repo.packageManager}` : ""}`
|
|
2040
2045
|
);
|
|
2041
|
-
info(`${repo.scripts.length}
|
|
2046
|
+
info(`${repo.scripts.length} command(s) \xB7 ${repo.docs.length} doc(s) \xB7 ${commits.length} commits read
|
|
2042
2047
|
`);
|
|
2043
|
-
info(color.bold("
|
|
2048
|
+
info(color.bold("Will propose:"));
|
|
2044
2049
|
for (const e of entries) {
|
|
2045
2050
|
const preview = e.content.split("\n").find((l) => l.trim() && !l.startsWith("#")) ?? "";
|
|
2046
2051
|
info(` ${color.cyan(e.type.padEnd(12))} ${e.title}`);
|
|
2047
2052
|
info(color.dim(` ${preview.slice(0, 76)}`));
|
|
2048
2053
|
}
|
|
2049
2054
|
if (opts.dryRun) {
|
|
2050
|
-
info(color.dim("\n--dry-run:
|
|
2055
|
+
info(color.dim("\n--dry-run: nothing was sent."));
|
|
2051
2056
|
return;
|
|
2052
2057
|
}
|
|
2053
2058
|
if (!opts.yes && !await confirm(`
|
|
2054
|
-
|
|
2055
|
-
warn("
|
|
2059
|
+
Send ${entries.length} proposal(s)?`)) {
|
|
2060
|
+
warn("Nothing was sent.");
|
|
2056
2061
|
return;
|
|
2057
2062
|
}
|
|
2058
2063
|
const base = opts.apiUrl.replace(/\/$/, "");
|
|
@@ -2063,73 +2068,74 @@ Enviar ${entries.length} proposta(s)?`)) {
|
|
|
2063
2068
|
signal: AbortSignal.timeout(3e4)
|
|
2064
2069
|
});
|
|
2065
2070
|
const data = await res.json().catch(() => ({}));
|
|
2066
|
-
if (res.status === 403) throw new Error("Token
|
|
2067
|
-
if (!res.ok) throw new Error(data.error ?? `
|
|
2068
|
-
ok(`${data.proposed}
|
|
2069
|
-
info(color.dim("
|
|
2071
|
+
if (res.status === 403) throw new Error("Token has no write scope \u2014 run `origem connect` again.");
|
|
2072
|
+
if (!res.ok) throw new Error(data.error ?? `Failed to propose (HTTP ${res.status}).`);
|
|
2073
|
+
ok(`${data.proposed} piece(s) proposed as one batch.`);
|
|
2074
|
+
info(color.dim("Accept or discard the batch in Origem \u2014 nothing was written to the harness yet."));
|
|
2070
2075
|
}
|
|
2071
2076
|
|
|
2072
2077
|
// src/index.ts
|
|
2073
|
-
var HELP = `${color.bold("origem")} \u2014
|
|
2078
|
+
var HELP = `${color.bold("origem")} \u2014 connects a repository to your Origem harness.
|
|
2074
2079
|
|
|
2075
|
-
${color.bold("
|
|
2076
|
-
origem connect [
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
origem spec
|
|
2080
|
-
origem doctor
|
|
2081
|
-
origem bootstrap
|
|
2082
|
-
origem propose [
|
|
2083
|
-
|
|
2084
|
-
origem apply [
|
|
2085
|
-
origem status [
|
|
2086
|
-
origem remove [
|
|
2087
|
-
origem login --key <t>
|
|
2088
|
-
origem logout Remove
|
|
2089
|
-
origem watch [
|
|
2090
|
-
origem hook install
|
|
2091
|
-
origem hook start (
|
|
2092
|
-
origem hook stop (
|
|
2093
|
-
origem help
|
|
2080
|
+
${color.bold("Usage:")}
|
|
2081
|
+
origem connect [options] Connect this repo to a harness (authorises in the
|
|
2082
|
+
browser, writes the files, installs skill + hooks).
|
|
2083
|
+
Run it again to sync.
|
|
2084
|
+
origem spec Open specs and the stage each one is in
|
|
2085
|
+
origem doctor Compare the harness against this repo (drift)
|
|
2086
|
+
origem bootstrap Read the repo and propose a whole harness
|
|
2087
|
+
origem propose [options] Suggest one change (stays pending until accepted
|
|
2088
|
+
on the site; writes nothing)
|
|
2089
|
+
origem apply [options] Pull the harness and write the config files
|
|
2090
|
+
origem status [options] What would change, and whether you edited a block
|
|
2091
|
+
origem remove [options] Revert everything Origem wrote (uses the lock)
|
|
2092
|
+
origem login --key <t> Store token + API in ~/.origem/config.json (0600)
|
|
2093
|
+
origem logout Remove the stored credentials
|
|
2094
|
+
origem watch [options] Local runner: executes queued commands with no session open
|
|
2095
|
+
origem hook install Install the hooks that link the session to Origem
|
|
2096
|
+
origem hook start (internal) marks the start of a turn
|
|
2097
|
+
origem hook stop (internal) reports the turn and fetches a command
|
|
2098
|
+
origem help This help
|
|
2094
2099
|
|
|
2095
|
-
${color.bold("
|
|
2096
|
-
-k, --key <token>
|
|
2097
|
-
--api-url <url>
|
|
2098
|
-
--scope <s> project (
|
|
2099
|
-
--dry-run
|
|
2100
|
-
-y, --yes
|
|
2101
|
-
--targets a,b
|
|
2102
|
-
--all
|
|
2103
|
-
--finish (hook stop)
|
|
2104
|
-
--quiet (hook stop)
|
|
2105
|
-
--bin <cmd> (hook install)
|
|
2106
|
-
--local (hook install)
|
|
2107
|
-
--once (watch)
|
|
2108
|
-
--no-hooks (connect)
|
|
2109
|
-
--type <t> (propose)
|
|
2110
|
-
--title <t> (propose)
|
|
2111
|
-
--content <t> (propose)
|
|
2112
|
-
--why <t> (propose)
|
|
2113
|
-
--card <id> (propose)
|
|
2114
|
-
--interval <s> (watch)
|
|
2115
|
-
--claude <path> (watch)
|
|
2116
|
-
--cwd <dir> (watch)
|
|
2117
|
-
--permission-mode (watch)
|
|
2100
|
+
${color.bold("Options:")}
|
|
2101
|
+
-k, --key <token> Capability token (or env ORIGEM_TOKEN)
|
|
2102
|
+
--api-url <url> API base URL (or env ORIGEM_API_URL)
|
|
2103
|
+
--scope <s> project (default) | user (~/.claude)
|
|
2104
|
+
--dry-run Print the plan only; write nothing
|
|
2105
|
+
-y, --yes Apply privileged config without asking
|
|
2106
|
+
--targets a,b Specific targets (e.g. claude,claude-settings,mcp)
|
|
2107
|
+
--all Every target (AGENTS.md, Cursor, Gemini, Copilot\u2026)
|
|
2108
|
+
--finish (hook stop) close the run instead of just reporting
|
|
2109
|
+
--quiet (hook stop) do not write errors to stderr
|
|
2110
|
+
--bin <cmd> (hook install) command stored in settings.json
|
|
2111
|
+
--local (hook install) write to .claude/settings.local.json
|
|
2112
|
+
--once (watch) a single pass, then exit (good for cron)
|
|
2113
|
+
--no-hooks (connect) do not install the session hooks
|
|
2114
|
+
--type <t> (propose) compartment: rule, boundary, memory, skill\u2026
|
|
2115
|
+
--title <t> (propose) title of the piece
|
|
2116
|
+
--content <t> (propose) content, or use --file <path>
|
|
2117
|
+
--why <t> (propose) why you are proposing it \u2014 required
|
|
2118
|
+
--card <id> (propose) change an existing piece instead of creating
|
|
2119
|
+
--interval <s> (watch) seconds between queue polls (default 5)
|
|
2120
|
+
--claude <path> (watch) Claude Code binary (default: auto-detect)
|
|
2121
|
+
--cwd <dir> (watch) directory to run in (default: current)
|
|
2122
|
+
--permission-mode (watch) passed through to Claude Code
|
|
2118
2123
|
|
|
2119
|
-
${color.bold("
|
|
2120
|
-
${color.bold("
|
|
2124
|
+
${color.bold("Default targets (Claude Code):")} ${CLAUDE_TARGETS.join(", ")}
|
|
2125
|
+
${color.bold("All:")} ${ALL_TARGETS.join(", ")}
|
|
2121
2126
|
|
|
2122
|
-
${color.bold("
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2127
|
+
${color.bold("Getting started:")} \`origem connect\` is the only command you need \u2014 it
|
|
2128
|
+
authorises in the browser (OAuth + PKCE, no token to paste), stores the credential,
|
|
2129
|
+
writes the harness to disk and installs the skill. \`login\`/\`apply\`/\`hook install\`
|
|
2130
|
+
are still there for whoever needs the steps apart.
|
|
2126
2131
|
|
|
2127
|
-
${color.bold("
|
|
2128
|
-
Origem
|
|
2129
|
-
|
|
2132
|
+
${color.bold("Driving from the site:")} with the hooks installed, every turn is reported to
|
|
2133
|
+
Origem and the reply may carry the next prompt from the queue \u2014 the session continues on
|
|
2134
|
+
its own. What travels back is a PROMPT, never shell: what runs is still decided by
|
|
2135
|
+
Claude Code's permission layer.
|
|
2130
2136
|
|
|
2131
|
-
${color.bold("
|
|
2132
|
-
|
|
2137
|
+
${color.bold("Trust:")} privileged files (settings.json with hooks/env, .mcp.json with
|
|
2138
|
+
command) require confirmation. Set ORIGEM_SIGNING_SECRET to require a signed payload.
|
|
2133
2139
|
`;
|
|
2134
2140
|
function parseArgs(argv) {
|
|
2135
2141
|
const cmd = argv[0] && !argv[0].startsWith("-") ? argv[0] : "help";
|
|
@@ -2162,7 +2168,7 @@ function parseArgs(argv) {
|
|
|
2162
2168
|
break;
|
|
2163
2169
|
case "--scope": {
|
|
2164
2170
|
const s = rest[++i];
|
|
2165
|
-
if (s !== "project" && s !== "user") throw new Error(
|
|
2171
|
+
if (s !== "project" && s !== "user") throw new Error(`Invalid --scope: ${s}`);
|
|
2166
2172
|
flags.scope = s;
|
|
2167
2173
|
break;
|
|
2168
2174
|
}
|
|
@@ -2193,7 +2199,7 @@ function parseArgs(argv) {
|
|
|
2193
2199
|
break;
|
|
2194
2200
|
case "--interval": {
|
|
2195
2201
|
const n = Number(rest[++i]);
|
|
2196
|
-
if (!Number.isFinite(n) || n < 2) throw new Error("--interval
|
|
2202
|
+
if (!Number.isFinite(n) || n < 2) throw new Error("--interval must be >= 2 (seconds)");
|
|
2197
2203
|
flags.interval = n;
|
|
2198
2204
|
break;
|
|
2199
2205
|
}
|
|
@@ -2234,7 +2240,7 @@ function parseArgs(argv) {
|
|
|
2234
2240
|
flags.permissionMode = rest[++i];
|
|
2235
2241
|
break;
|
|
2236
2242
|
default:
|
|
2237
|
-
throw new Error(`
|
|
2243
|
+
throw new Error(`Unknown option: ${a}`);
|
|
2238
2244
|
}
|
|
2239
2245
|
}
|
|
2240
2246
|
return { cmd, flags };
|
|
@@ -2262,7 +2268,7 @@ async function main() {
|
|
|
2262
2268
|
}
|
|
2263
2269
|
case "propose": {
|
|
2264
2270
|
if (!flags.type || !flags.title) {
|
|
2265
|
-
throw new Error("
|
|
2271
|
+
throw new Error("Usage: origem propose --type <compartment> --title <title> --content <text> --why <reason>");
|
|
2266
2272
|
}
|
|
2267
2273
|
await runPropose({
|
|
2268
2274
|
apiUrl: resolveApiUrl(flags.apiUrl),
|
|
@@ -2355,7 +2361,7 @@ async function main() {
|
|
|
2355
2361
|
quiet: flags.quiet
|
|
2356
2362
|
});
|
|
2357
2363
|
} else {
|
|
2358
|
-
err("
|
|
2364
|
+
err("Usage: origem hook install | origem hook start | origem hook stop");
|
|
2359
2365
|
process.exitCode = 1;
|
|
2360
2366
|
}
|
|
2361
2367
|
} catch (e) {
|
|
@@ -2365,7 +2371,7 @@ async function main() {
|
|
|
2365
2371
|
break;
|
|
2366
2372
|
}
|
|
2367
2373
|
default:
|
|
2368
|
-
err(`
|
|
2374
|
+
err(`Unknown command: ${cmd}`);
|
|
2369
2375
|
info(HELP);
|
|
2370
2376
|
process.exitCode = 1;
|
|
2371
2377
|
}
|