@nanopm/cli 0.1.5 → 0.1.7
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/dist/index.js +258 -137
- package/package.json +1 -1
- package/skills/direction/SKILL.md +1 -1
- package/skills/market/SKILL.md +1 -1
- package/skills/needs/SKILL.md +1 -1
- package/skills/onboard/SKILL.md +26 -14
- package/skills/opportunities/SKILL.md +1 -1
- package/skills/personas/SKILL.md +1 -1
- package/skills/pitch/SKILL.md +1 -1
- package/skills/problem-mapper/SKILL.md +1 -1
- package/skills/talk/skill.json +1 -1
package/dist/index.js
CHANGED
|
@@ -105,7 +105,7 @@ var CAPABILITIES = [
|
|
|
105
105
|
* `projects.website_url`, never a model's choice, and only that host and three fixed
|
|
106
106
|
* paths are fetched. That is what lets a skill holding the founder's repository read
|
|
107
107
|
* their landing page without an outbound socket sitting next to private code, which is
|
|
108
|
-
* the combination the taint rule exists to prevent (docs
|
|
108
|
+
* the combination the taint rule exists to prevent (docs/✅nanopm-60-onboarding-end-to-end.md §6).
|
|
109
109
|
*
|
|
110
110
|
* Capped nowhere, and it does not taint: there is no tool here to misuse.
|
|
111
111
|
*/
|
|
@@ -139,7 +139,7 @@ var CAPABILITIES = [
|
|
|
139
139
|
/** Maintain the market view: write_market, and list_market under `memory`. */
|
|
140
140
|
"market",
|
|
141
141
|
/**
|
|
142
|
-
* A conversation with Nano (docs
|
|
142
|
+
* A conversation with Nano (docs/✅nanopm-68-talk-to-nano.md): read the opportunity map and the
|
|
143
143
|
* board and change them as the founder asked — the tools in `TALK_TOOLS`. Nothing else: no
|
|
144
144
|
* repository, no web, no data. Capped nowhere: every change is one the founder can make from a
|
|
145
145
|
* sheet, and only what they asked for is done.
|
|
@@ -374,7 +374,7 @@ var SkillManifestSchema = z2.object({
|
|
|
374
374
|
),
|
|
375
375
|
/**
|
|
376
376
|
* The cap for a run with `params.depth: 'deep'`, when the skill has one: the deep needs map
|
|
377
|
-
* runs ten sessions and never fit the quick map's forty minutes (docs
|
|
377
|
+
* runs ten sessions and never fit the quick map's forty minutes (docs/✅nanopm-63-quick-first.md §4).
|
|
378
378
|
*/
|
|
379
379
|
timeout_deep: z2.string().refine(
|
|
380
380
|
(t) => {
|
|
@@ -872,7 +872,7 @@ var CHAINED = {
|
|
|
872
872
|
// and the crew runs when the founder asks for it from the business card.
|
|
873
873
|
onboard: ["market"],
|
|
874
874
|
// A problem list is what solutions hang under, so a fresh list is followed by fresh
|
|
875
|
-
// solutions. **The problem crew itself is retired** (docs
|
|
875
|
+
// solutions. **The problem crew itself is retired** (docs/✅nanopm-59-opportunity-map.md,
|
|
876
876
|
// Guillaume, 2026-09-25): the opportunity map replaced it, and nothing queues it any more —
|
|
877
877
|
// not the market read, not a review, not a watch, not the founder leaving Voices. A map
|
|
878
878
|
// someone still runs by hand keeps this edge, because every bet still hangs off a problem
|
|
@@ -880,7 +880,7 @@ var CHAINED = {
|
|
|
880
880
|
problems: ["solutions"],
|
|
881
881
|
// The pitch crew runs after the market read, which is the one automatic run that changes
|
|
882
882
|
// what the pitch can say without anyone asking: an "unlike" clause needs competitors. It
|
|
883
|
-
// proposes beside a confirmed pitch and never over it (docs
|
|
883
|
+
// proposes beside a confirmed pitch and never over it (docs/✅nanopm-52-pitch-crew.md).
|
|
884
884
|
//
|
|
885
885
|
// The direction crew **was** chained here too, for the same reason — a vision rests on who
|
|
886
886
|
// else exists — then after `onboard`, and since 2026-10-01 after nothing: see the note on
|
|
@@ -891,7 +891,7 @@ var CHAINED = {
|
|
|
891
891
|
// for both, and gets to say something different to each.
|
|
892
892
|
//
|
|
893
893
|
// **The persona crew follows a from-scratch start's market run, and no other**
|
|
894
|
-
// (docs
|
|
894
|
+
// (docs/✅nanopm-54-personas-by-the-crew.md; docs/✅nanopm-63-quick-first.md §2). A start writes a
|
|
895
895
|
// sentence per person, and the crew draws the profile. It followed a scan's market run too
|
|
896
896
|
// until 2026-09-28, to put a critic over the scan's one pass — and the redo, with the pitch
|
|
897
897
|
// behind it, stood between the founder and the first opportunities while redrawing what the
|
|
@@ -900,7 +900,7 @@ var CHAINED = {
|
|
|
900
900
|
// start: the daemon claims in queue order, and the pitch and the map read what it wrote.
|
|
901
901
|
//
|
|
902
902
|
// **The first opportunity map follows the onboarding's own market run, and no other**
|
|
903
|
-
// (docs
|
|
903
|
+
// (docs/✅nanopm-59-opportunity-map.md §8). `ONLY_AFTER` below holds it there: a market run from
|
|
904
904
|
// the card or the terminal queues no map, so a founder's refresh of the landscape never
|
|
905
905
|
// redraws it. It reads the card, the personas and their needs the onboarding wrote, and the
|
|
906
906
|
// market just read; a needs map is one source among the others, never a constraint
|
|
@@ -914,7 +914,7 @@ var CHAINED = {
|
|
|
914
914
|
// draw, on the founder's word; drawn there, it queues nothing and the page offers *Rank
|
|
915
915
|
// again*. The deep map still follows a deep needs map queued after a start (`AFTER_DEEP`).
|
|
916
916
|
market: ["personas", "pitch", "opportunities"],
|
|
917
|
-
// **Solutions follow the map, but not through this table** (docs
|
|
917
|
+
// **Solutions follow the map, but not through this table** (docs/✅nanopm-64-solutions-from-
|
|
918
918
|
// opportunities.md §3): the store queues a Flash the moment a branch ranked high enough has
|
|
919
919
|
// leaves with no solution — when the map is recorded, and when the founder moves, confirms or
|
|
920
920
|
// sets one aside (`queue_solutions_due`). Only some maps call for one, and a map is not the only
|
|
@@ -964,7 +964,7 @@ var SKILL_SENTENCE = {
|
|
|
964
964
|
problems: "working out the problems",
|
|
965
965
|
market: "reading the market",
|
|
966
966
|
// Kept: `research` is no longer chained or run from anywhere a founder can see, but the
|
|
967
|
-
// problem crew still calls it inline for its look outside (docs
|
|
967
|
+
// problem crew still calls it inline for its look outside (docs/✅nanopm-55-market-crew.md
|
|
968
968
|
// §9), and a job of it in the journal should still read as a sentence.
|
|
969
969
|
research: "reading the market",
|
|
970
970
|
personas: "working out who it is for",
|
|
@@ -3856,10 +3856,10 @@ function parsePrintResult(stdout) {
|
|
|
3856
3856
|
}
|
|
3857
3857
|
|
|
3858
3858
|
// ../daemon/src/daemon.ts
|
|
3859
|
-
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile7, rm as
|
|
3859
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
3860
3860
|
import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
|
|
3861
3861
|
import { hostname as hostname2 } from "os";
|
|
3862
|
-
import { join as
|
|
3862
|
+
import { join as join11 } from "path";
|
|
3863
3863
|
import { setTimeout as sleep } from "timers/promises";
|
|
3864
3864
|
|
|
3865
3865
|
// ../daemon/src/awake.ts
|
|
@@ -3878,6 +3878,71 @@ function stayAwake(platform = process.platform) {
|
|
|
3878
3878
|
}
|
|
3879
3879
|
}
|
|
3880
3880
|
|
|
3881
|
+
// ../daemon/src/update.ts
|
|
3882
|
+
import { execFile as execFile2 } from "child_process";
|
|
3883
|
+
import { existsSync, realpathSync } from "fs";
|
|
3884
|
+
import { rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
3885
|
+
import { homedir as homedir2 } from "os";
|
|
3886
|
+
import { join as join4, sep } from "path";
|
|
3887
|
+
import { promisify as promisify2 } from "util";
|
|
3888
|
+
var PACKAGE = "@nanopm/cli";
|
|
3889
|
+
var LATEST_URL = "https://registry.npmjs.org/@nanopm%2fcli/latest";
|
|
3890
|
+
var RELEASE = /^\d+\.\d+\.\d+$/;
|
|
3891
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3892
|
+
var root = (home = homedir2()) => join4(home, ".nanopm");
|
|
3893
|
+
var appDir = (home) => join4(root(home), "app");
|
|
3894
|
+
var offFlag = (home) => join4(root(home), "no-auto-update");
|
|
3895
|
+
function installKind(bin = process.argv[1], home = homedir2()) {
|
|
3896
|
+
if (!bin) return "other";
|
|
3897
|
+
let real = bin;
|
|
3898
|
+
try {
|
|
3899
|
+
real = realpathSync(bin);
|
|
3900
|
+
} catch {
|
|
3901
|
+
}
|
|
3902
|
+
return real.startsWith(appDir(home) + sep) ? "installer" : "other";
|
|
3903
|
+
}
|
|
3904
|
+
function newer(latest, current) {
|
|
3905
|
+
if (!latest || !RELEASE.test(latest) || !RELEASE.test(current)) return false;
|
|
3906
|
+
const a = latest.split(".").map(Number);
|
|
3907
|
+
const b = current.split(".").map(Number);
|
|
3908
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
3909
|
+
return false;
|
|
3910
|
+
}
|
|
3911
|
+
async function latestVersion(fetchImpl = fetch) {
|
|
3912
|
+
try {
|
|
3913
|
+
const res = await fetchImpl(LATEST_URL, { signal: AbortSignal.timeout(5e3) });
|
|
3914
|
+
if (!res.ok) return null;
|
|
3915
|
+
const { version } = await res.json();
|
|
3916
|
+
return typeof version === "string" && RELEASE.test(version) ? version : null;
|
|
3917
|
+
} catch {
|
|
3918
|
+
return null;
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
async function installVersion(version, home = homedir2()) {
|
|
3922
|
+
if (!RELEASE.test(version)) return { ok: false, why: `not a version: ${version}` };
|
|
3923
|
+
const runtime = join4(root(home), "runtime", "bin");
|
|
3924
|
+
try {
|
|
3925
|
+
await execFileAsync2(join4(runtime, "npm"), ["install", "-g", "--prefix", appDir(home), `${PACKAGE}@${version}`, "--no-fund", "--no-audit", "--loglevel=error"], {
|
|
3926
|
+
timeout: 5 * 6e4,
|
|
3927
|
+
env: { ...process.env, PATH: `${runtime}:${process.env.PATH ?? ""}`, NPM_CONFIG_UPDATE_NOTIFIER: "false" }
|
|
3928
|
+
});
|
|
3929
|
+
return { ok: true };
|
|
3930
|
+
} catch (err) {
|
|
3931
|
+
const failed = err;
|
|
3932
|
+
return { ok: false, why: (failed.stderr?.trim() || failed.message || String(err)).split("\n").slice(-3).join(" ") };
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
function autoUpdateOff(home = homedir2()) {
|
|
3936
|
+
return existsSync(offFlag(home));
|
|
3937
|
+
}
|
|
3938
|
+
async function setAutoUpdate(on, home = homedir2()) {
|
|
3939
|
+
if (on) await rm2(offFlag(home), { force: true });
|
|
3940
|
+
else await writeFile3(offFlag(home), "Automatic updates are off: `nanopm update --auto on` turns them back on.\n");
|
|
3941
|
+
}
|
|
3942
|
+
function shouldUpdate(s) {
|
|
3943
|
+
return s.kind === "installer" && !s.off && !s.quickBusy && newer(s.latest, s.current);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3881
3946
|
// ../daemon/src/mcp.ts
|
|
3882
3947
|
function memoryMcpCommand(env = process.env) {
|
|
3883
3948
|
const override = env.NANOPM_MCP_COMMAND;
|
|
@@ -4865,7 +4930,7 @@ async function appStoreSearch(term, country, limit, opts = {}) {
|
|
|
4865
4930
|
|
|
4866
4931
|
// ../connectors/src/stores/play.ts
|
|
4867
4932
|
var SEARCH2 = "https://play.google.com/store/search";
|
|
4868
|
-
var
|
|
4933
|
+
var PACKAGE2 = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){2,}$/i;
|
|
4869
4934
|
function playSearchUrl(term, country) {
|
|
4870
4935
|
const url = new URL(SEARCH2);
|
|
4871
4936
|
url.searchParams.set("q", term);
|
|
@@ -4889,7 +4954,7 @@ function blobs(html) {
|
|
|
4889
4954
|
function cards(node, found = []) {
|
|
4890
4955
|
if (!Array.isArray(node)) return found;
|
|
4891
4956
|
const head = node[0];
|
|
4892
|
-
if (Array.isArray(head) && head.length === 2 && typeof head[0] === "string" &&
|
|
4957
|
+
if (Array.isArray(head) && head.length === 2 && typeof head[0] === "string" && PACKAGE2.test(head[0]) && head[1] === 7) {
|
|
4893
4958
|
found.push(node);
|
|
4894
4959
|
return found;
|
|
4895
4960
|
}
|
|
@@ -5022,13 +5087,13 @@ async function sendDigestFor(client, accessToken, jobId, fetchImpl = fetch) {
|
|
|
5022
5087
|
}
|
|
5023
5088
|
|
|
5024
5089
|
// ../daemon/src/deploys.ts
|
|
5025
|
-
import { execFile as
|
|
5026
|
-
import { promisify as
|
|
5027
|
-
var
|
|
5090
|
+
import { execFile as execFile3 } from "child_process";
|
|
5091
|
+
import { promisify as promisify3 } from "util";
|
|
5092
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
5028
5093
|
var SETTLE_HOURS = 3;
|
|
5029
5094
|
var MIN_COMMITS = 3;
|
|
5030
5095
|
async function git(cwd, args) {
|
|
5031
|
-
const { stdout } = await
|
|
5096
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
|
|
5032
5097
|
return stdout.trim();
|
|
5033
5098
|
}
|
|
5034
5099
|
async function defaultBranch(cwd) {
|
|
@@ -5124,17 +5189,17 @@ async function reviewWithoutData(client, job) {
|
|
|
5124
5189
|
}
|
|
5125
5190
|
|
|
5126
5191
|
// ../daemon/src/skills.ts
|
|
5127
|
-
import { existsSync, readdirSync } from "fs";
|
|
5192
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
5128
5193
|
import { readFile as readFile2 } from "fs/promises";
|
|
5129
|
-
import { dirname as dirname2, join as
|
|
5194
|
+
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
5130
5195
|
function findSkillsDir(env = process.env) {
|
|
5131
5196
|
if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
|
|
5132
5197
|
const candidates = [
|
|
5133
|
-
|
|
5198
|
+
join5(process.cwd(), "skills"),
|
|
5134
5199
|
...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
|
|
5135
5200
|
];
|
|
5136
5201
|
for (const dir of candidates) {
|
|
5137
|
-
if (
|
|
5202
|
+
if (existsSync2(join5(dir, "onboard", "skill.json"))) return dir;
|
|
5138
5203
|
}
|
|
5139
5204
|
throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
|
|
5140
5205
|
}
|
|
@@ -5144,9 +5209,9 @@ function skillDirs(skillsDir, env = process.env) {
|
|
|
5144
5209
|
}
|
|
5145
5210
|
async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
5146
5211
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
|
|
5147
|
-
const dir = skillDirs(skillsDir).map((d) =>
|
|
5212
|
+
const dir = skillDirs(skillsDir).map((d) => join5(d, name)).find((d) => existsSync2(join5(d, "skill.json")));
|
|
5148
5213
|
if (!dir) throw new Error(`Unknown skill "${name}"`);
|
|
5149
|
-
const raw = JSON.parse(await readFile2(
|
|
5214
|
+
const raw = JSON.parse(await readFile2(join5(dir, "skill.json"), "utf8"));
|
|
5150
5215
|
let manifest;
|
|
5151
5216
|
try {
|
|
5152
5217
|
manifest = parseSkillManifest(raw);
|
|
@@ -5155,7 +5220,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
|
|
|
5155
5220
|
}
|
|
5156
5221
|
const tainted = taintedByTheWeb(manifest);
|
|
5157
5222
|
if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
|
|
5158
|
-
const body = await readFile2(
|
|
5223
|
+
const body = await readFile2(join5(dir, "SKILL.md"), "utf8");
|
|
5159
5224
|
return { manifest, body, dir };
|
|
5160
5225
|
}
|
|
5161
5226
|
var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
|
|
@@ -5190,7 +5255,7 @@ function renderPrompt(template, params) {
|
|
|
5190
5255
|
var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
|
|
5191
5256
|
var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
|
|
5192
5257
|
async function clonelessSkills(skillsDir = findSkillsDir()) {
|
|
5193
|
-
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() &&
|
|
5258
|
+
const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join5(skillsDir, d.name, "skill.json"))).map((d) => d.name);
|
|
5194
5259
|
const out = [];
|
|
5195
5260
|
for (const name of names) {
|
|
5196
5261
|
try {
|
|
@@ -5402,9 +5467,9 @@ async function runMappingJob({ client, job, adapter, spec, mcpServers, onEvent,
|
|
|
5402
5467
|
};
|
|
5403
5468
|
const { data: rootId, error: rootError } = await client.rpc("mapping_run_root", { p_job_id: job.id });
|
|
5404
5469
|
if (rootError) throw new Error(rootError.message);
|
|
5405
|
-
const { data:
|
|
5470
|
+
const { data: root2, error: readError } = await client.from("jobs").select("result").eq("id", rootId).single();
|
|
5406
5471
|
if (readError) throw new Error(readError.message);
|
|
5407
|
-
const saved =
|
|
5472
|
+
const saved = root2.result;
|
|
5408
5473
|
if (saved?.mapping_receipt) return { status: "succeeded", result: saved.mapping_audit?.checked.summary ?? `Problem map published: ${saved.mapping_receipt.refs.join(", ")}` };
|
|
5409
5474
|
const previousUsage = mappingUsage(saved?.mapping_checkpoints ?? []);
|
|
5410
5475
|
const result = await runProblemMapping({
|
|
@@ -5520,7 +5585,7 @@ var UNANSWERED = /structured[ _]output|returned no result/i;
|
|
|
5520
5585
|
|
|
5521
5586
|
// ../daemon/src/site.ts
|
|
5522
5587
|
import { readFile as readFile3 } from "fs/promises";
|
|
5523
|
-
import { join as
|
|
5588
|
+
import { join as join6 } from "path";
|
|
5524
5589
|
var SITE_PATHS = ["/", "/about", "/pricing"];
|
|
5525
5590
|
var PAGE_CHARS = 6e3;
|
|
5526
5591
|
var SITE_TIMEOUT_MS = 2e4;
|
|
@@ -5643,7 +5708,7 @@ function siteFromSearch(name, hits) {
|
|
|
5643
5708
|
}
|
|
5644
5709
|
async function read(repoRoot, name) {
|
|
5645
5710
|
try {
|
|
5646
|
-
const text2 = await readFile3(
|
|
5711
|
+
const text2 = await readFile3(join6(repoRoot, name), "utf8");
|
|
5647
5712
|
return text2.length > 2e5 ? null : text2;
|
|
5648
5713
|
} catch {
|
|
5649
5714
|
return null;
|
|
@@ -5698,7 +5763,7 @@ async function fromReadme(repoRoot) {
|
|
|
5698
5763
|
|
|
5699
5764
|
// ../daemon/src/repo-docs.ts
|
|
5700
5765
|
import { readFile as readFile4, readdir, stat } from "fs/promises";
|
|
5701
|
-
import { join as
|
|
5766
|
+
import { join as join7, relative } from "path";
|
|
5702
5767
|
var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
|
|
5703
5768
|
var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
|
|
5704
5769
|
var DOC = /\.(md|mdx|txt)$/i;
|
|
@@ -5716,7 +5781,7 @@ async function readDocs(cwd, opts = {}) {
|
|
|
5716
5781
|
return;
|
|
5717
5782
|
}
|
|
5718
5783
|
for (const entry of entries) {
|
|
5719
|
-
const full =
|
|
5784
|
+
const full = join7(dir, entry.name);
|
|
5720
5785
|
if (SKIP.test(`/${relative(cwd, full)}`)) continue;
|
|
5721
5786
|
if (entry.isDirectory()) await walk(full, depth + 1);
|
|
5722
5787
|
else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
|
|
@@ -6827,7 +6892,7 @@ async function attempt4({ adapter, spec, onEvent, role: which, prompt, say }) {
|
|
|
6827
6892
|
harnessWeb: which === "scout" ? spec.harnessWeb : false,
|
|
6828
6893
|
// A role runs on its own model when its manifest names one: the scout on Sonnet, as spec 55
|
|
6829
6894
|
// wanted and as it never did, because every role inherited the coordinator's — the user's
|
|
6830
|
-
// default (docs
|
|
6895
|
+
// default (docs/✅nanopm-57-needs-map.md §10).
|
|
6831
6896
|
model: skill.manifest.model ?? spec.model,
|
|
6832
6897
|
outputSchema: schema,
|
|
6833
6898
|
maxTurns: skill.manifest.maxTurns,
|
|
@@ -7389,15 +7454,15 @@ async function mapRows(client, projectId, persona) {
|
|
|
7389
7454
|
function scopeNodes(rows2, scope) {
|
|
7390
7455
|
return scope ? descendants(rows2, scope) : [...rows2];
|
|
7391
7456
|
}
|
|
7392
|
-
function descendants(rows2,
|
|
7457
|
+
function descendants(rows2, root2) {
|
|
7393
7458
|
const byId = new Map(rows2.map((r) => [r.id, r]));
|
|
7394
7459
|
const current = (id) => {
|
|
7395
7460
|
let n = id ? byId.get(id) : void 0;
|
|
7396
7461
|
for (let hops = 0; n && n.status === "replaced" && n.replaced_by && hops < 50; hops += 1) n = byId.get(n.replaced_by);
|
|
7397
7462
|
return n && n.status !== "replaced" ? n : null;
|
|
7398
7463
|
};
|
|
7399
|
-
const out = [
|
|
7400
|
-
const ids = /* @__PURE__ */ new Set([
|
|
7464
|
+
const out = [root2];
|
|
7465
|
+
const ids = /* @__PURE__ */ new Set([root2.id]);
|
|
7401
7466
|
for (let grew = true; grew; ) {
|
|
7402
7467
|
grew = false;
|
|
7403
7468
|
for (const r of rows2) {
|
|
@@ -7536,7 +7601,7 @@ var OpportunitiesParams = z13.object({
|
|
|
7536
7601
|
chain: z13.boolean().optional(),
|
|
7537
7602
|
repo_path: z13.string().optional(),
|
|
7538
7603
|
// The daemon's: a run chained as background work is claimed once no quick job is queued
|
|
7539
|
-
// (docs
|
|
7604
|
+
// (docs/✅nanopm-63-quick-first.md). The deep map after the deep needs map carries it.
|
|
7540
7605
|
background: z13.boolean().optional()
|
|
7541
7606
|
}).strict();
|
|
7542
7607
|
function opportunitiesAsk(params) {
|
|
@@ -7706,7 +7771,7 @@ function opportunitiesOutputSchema(which) {
|
|
|
7706
7771
|
// ../daemon/src/crew.ts
|
|
7707
7772
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
7708
7773
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
7709
|
-
import { join as
|
|
7774
|
+
import { join as join8 } from "path";
|
|
7710
7775
|
var READ_ONLY = {
|
|
7711
7776
|
read_repo: /^(Read|Glob|Grep)$/,
|
|
7712
7777
|
read_data: /(^|__)(list_connections|describe_source|query)$/
|
|
@@ -7721,14 +7786,14 @@ async function attempt6({ adapter, spec, onEvent, skill: skillName, name, schema
|
|
|
7721
7786
|
if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
|
|
7722
7787
|
const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
|
|
7723
7788
|
if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
|
|
7724
|
-
const runDir = dir ?
|
|
7789
|
+
const runDir = dir ? join8(spec.runDir, dir) : spec.runDir;
|
|
7725
7790
|
if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
|
|
7726
7791
|
let result;
|
|
7727
7792
|
let usage = null;
|
|
7728
7793
|
await onEvent({ type: "text", text: say });
|
|
7729
7794
|
for await (const event of adapter.run({
|
|
7730
7795
|
...spec,
|
|
7731
|
-
// A role that may not read the code does not stand in it (docs
|
|
7796
|
+
// A role that may not read the code does not stand in it (docs/✅nanopm-71-deep-dive-lenses.md).
|
|
7732
7797
|
cwd: grant.includes("read_repo") ? spec.cwd : runDir,
|
|
7733
7798
|
runDir,
|
|
7734
7799
|
sessionId: randomUUID8(),
|
|
@@ -9398,21 +9463,21 @@ var told = (r) => ({
|
|
|
9398
9463
|
});
|
|
9399
9464
|
|
|
9400
9465
|
// ../daemon/src/objective-definitions.ts
|
|
9401
|
-
import { execFile as
|
|
9466
|
+
import { execFile as execFile5 } from "child_process";
|
|
9402
9467
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
9403
|
-
import { existsSync as
|
|
9468
|
+
import { existsSync as existsSync3 } from "fs";
|
|
9404
9469
|
import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
|
|
9405
|
-
import { join as
|
|
9406
|
-
import { promisify as
|
|
9470
|
+
import { join as join9 } from "path";
|
|
9471
|
+
import { promisify as promisify5 } from "util";
|
|
9407
9472
|
|
|
9408
9473
|
// ../daemon/src/git.ts
|
|
9409
|
-
import { execFile as
|
|
9410
|
-
import { promisify as
|
|
9411
|
-
var
|
|
9474
|
+
import { execFile as execFile4 } from "child_process";
|
|
9475
|
+
import { promisify as promisify4 } from "util";
|
|
9476
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
9412
9477
|
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
9413
9478
|
var WIKI_DIR = ".nanopm/wiki/docs";
|
|
9414
9479
|
async function git2(cwd, args) {
|
|
9415
|
-
const { stdout } = await
|
|
9480
|
+
const { stdout } = await execFileAsync4("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER, timeout: 2e4 });
|
|
9416
9481
|
return stdout.trim();
|
|
9417
9482
|
}
|
|
9418
9483
|
async function listTrackedFiles(cwd) {
|
|
@@ -9511,7 +9576,7 @@ function renderTrackedFiles(files, max = TRACKED_FILES_MAX, wiki = []) {
|
|
|
9511
9576
|
}
|
|
9512
9577
|
|
|
9513
9578
|
// ../daemon/src/objective-definitions.ts
|
|
9514
|
-
var
|
|
9579
|
+
var execFileAsync5 = promisify5(execFile5);
|
|
9515
9580
|
var DEFINITIONS_TIMEOUT_MS = 9e4;
|
|
9516
9581
|
var DEFINITIONS_THINKING_TOKENS = 2e3;
|
|
9517
9582
|
var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
|
|
@@ -9520,7 +9585,7 @@ var COUNTING_LINES_MAX = 80;
|
|
|
9520
9585
|
var PER_FILE = 4;
|
|
9521
9586
|
async function countingLines(cwd) {
|
|
9522
9587
|
try {
|
|
9523
|
-
const { stdout } = await
|
|
9588
|
+
const { stdout } = await execFileAsync5("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
|
|
9524
9589
|
cwd,
|
|
9525
9590
|
maxBuffer: 16 * 1024 * 1024,
|
|
9526
9591
|
timeout: 5e3
|
|
@@ -9545,7 +9610,7 @@ function pickLines(raw) {
|
|
|
9545
9610
|
}
|
|
9546
9611
|
async function killRecorded(runDir) {
|
|
9547
9612
|
try {
|
|
9548
|
-
const pid = Number((await readFile5(
|
|
9613
|
+
const pid = Number((await readFile5(join9(runDir, "harness.pid"), "utf8")).trim());
|
|
9549
9614
|
if (pid > 1) process.kill(pid, "SIGKILL");
|
|
9550
9615
|
} catch {
|
|
9551
9616
|
}
|
|
@@ -9565,7 +9630,7 @@ async function readDefinitions({ adapter, spec, publish: publish5, journal, hasR
|
|
|
9565
9630
|
if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
|
|
9566
9631
|
try {
|
|
9567
9632
|
const skill = await loadSkill("define-objective");
|
|
9568
|
-
const runDir =
|
|
9633
|
+
const runDir = join9(spec.runDir, "definitions");
|
|
9569
9634
|
await mkdir3(runDir, { recursive: true, mode: 448 });
|
|
9570
9635
|
const schema = definitionsOutputSchema();
|
|
9571
9636
|
const [lines, listed] = await Promise.all([countingLines(spec.cwd), listTrackedFiles(spec.cwd)]);
|
|
@@ -9627,7 +9692,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
|
|
|
9627
9692
|
if (!result) throw new Error("no answer");
|
|
9628
9693
|
const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
|
|
9629
9694
|
if (!parsed.success) throw new Error("an answer in the wrong shape");
|
|
9630
|
-
const checked = checkProposals(parsed.data, (file) =>
|
|
9695
|
+
const checked = checkProposals(parsed.data, (file) => existsSync3(join9(spec.cwd, file)));
|
|
9631
9696
|
const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
|
|
9632
9697
|
const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
|
|
9633
9698
|
const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
|
|
@@ -9900,10 +9965,10 @@ function localDate(timezone, at) {
|
|
|
9900
9965
|
|
|
9901
9966
|
// ../daemon/src/run-job.ts
|
|
9902
9967
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
9903
|
-
import { existsSync as
|
|
9904
|
-
import { mkdir as mkdir4, readFile as readFile6, rm as
|
|
9905
|
-
import { homedir as
|
|
9906
|
-
import { join as
|
|
9968
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9969
|
+
import { mkdir as mkdir4, readFile as readFile6, rm as rm3 } from "fs/promises";
|
|
9970
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
9971
|
+
import { join as join10, resolve as resolve4 } from "path";
|
|
9907
9972
|
|
|
9908
9973
|
// ../daemon/src/events.ts
|
|
9909
9974
|
var JobEventWriter = class {
|
|
@@ -9984,13 +10049,13 @@ function lighten(value) {
|
|
|
9984
10049
|
}
|
|
9985
10050
|
|
|
9986
10051
|
// ../daemon/src/repo.ts
|
|
9987
|
-
import { execFile as
|
|
9988
|
-
import { promisify as
|
|
9989
|
-
var
|
|
10052
|
+
import { execFile as execFile6 } from "child_process";
|
|
10053
|
+
import { promisify as promisify6 } from "util";
|
|
10054
|
+
var execFileAsync6 = promisify6(execFile6);
|
|
9990
10055
|
var MAX_BUFFER2 = 4 * 1024 * 1024;
|
|
9991
10056
|
var TIMEOUT_MS = 6e4;
|
|
9992
10057
|
async function git3(cwd, args, timeoutMs = TIMEOUT_MS) {
|
|
9993
|
-
const { stdout } = await
|
|
10058
|
+
const { stdout } = await execFileAsync6("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: timeoutMs });
|
|
9994
10059
|
return stdout.trim();
|
|
9995
10060
|
}
|
|
9996
10061
|
var RepoError = class extends Error {
|
|
@@ -10061,7 +10126,7 @@ async function pushBranch(cwd, name) {
|
|
|
10061
10126
|
const state = await readState(cwd);
|
|
10062
10127
|
if (!state.hasRemote) return { pushed: false, reason: "this repository has no origin remote" };
|
|
10063
10128
|
try {
|
|
10064
|
-
const { stderr } = await
|
|
10129
|
+
const { stderr } = await execFileAsync6("git", ["-C", cwd, "push", "--set-upstream", "origin", name], {
|
|
10065
10130
|
maxBuffer: MAX_BUFFER2,
|
|
10066
10131
|
timeout: 12e4
|
|
10067
10132
|
});
|
|
@@ -10341,7 +10406,7 @@ async function runJob(job, ctx) {
|
|
|
10341
10406
|
return outcome;
|
|
10342
10407
|
}
|
|
10343
10408
|
async function execute(job, skill, ctx, events) {
|
|
10344
|
-
const runDir =
|
|
10409
|
+
const runDir = join10(nanopmHome(), "run", job.id);
|
|
10345
10410
|
await mkdir4(runDir, { recursive: true, mode: 448 });
|
|
10346
10411
|
const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
|
|
10347
10412
|
const sessionId = resumeSessionId ?? randomUUID12();
|
|
@@ -10386,7 +10451,7 @@ async function execute(job, skill, ctx, events) {
|
|
|
10386
10451
|
clearTimeout(timer);
|
|
10387
10452
|
clearInterval(heartbeat2);
|
|
10388
10453
|
clearInterval(cancelPoll);
|
|
10389
|
-
await
|
|
10454
|
+
await rm3(runDir, { recursive: true, force: true });
|
|
10390
10455
|
}
|
|
10391
10456
|
}
|
|
10392
10457
|
const quickRun2 = QUICK_RUNS[job.skill];
|
|
@@ -10413,8 +10478,8 @@ async function execute(job, skill, ctx, events) {
|
|
|
10413
10478
|
clearTimeout(timer);
|
|
10414
10479
|
clearInterval(heartbeat2);
|
|
10415
10480
|
clearInterval(cancelPoll);
|
|
10416
|
-
await
|
|
10417
|
-
await
|
|
10481
|
+
await rm3(runDir, { recursive: true, force: true });
|
|
10482
|
+
await rm3(cwd2, { recursive: true, force: true });
|
|
10418
10483
|
}
|
|
10419
10484
|
}
|
|
10420
10485
|
const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
|
|
@@ -10622,7 +10687,7 @@ async function execute(job, skill, ctx, events) {
|
|
|
10622
10687
|
clearInterval(heartbeat2);
|
|
10623
10688
|
clearInterval(cancelPoll);
|
|
10624
10689
|
await definitionsRead;
|
|
10625
|
-
await
|
|
10690
|
+
await rm3(runDir, { recursive: true, force: true });
|
|
10626
10691
|
}
|
|
10627
10692
|
if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
|
|
10628
10693
|
outcome = {
|
|
@@ -10704,7 +10769,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
|
|
|
10704
10769
|
}
|
|
10705
10770
|
async function machineWebSearch() {
|
|
10706
10771
|
try {
|
|
10707
|
-
const parsed = JSON.parse(await readFile6(
|
|
10772
|
+
const parsed = JSON.parse(await readFile6(join10(nanopmHome(), "daemon.json"), "utf8"));
|
|
10708
10773
|
return parsed.webSearch === true;
|
|
10709
10774
|
} catch {
|
|
10710
10775
|
return false;
|
|
@@ -10746,7 +10811,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
|
|
|
10746
10811
|
return lines.join("\n");
|
|
10747
10812
|
}
|
|
10748
10813
|
function scratchDir(jobId) {
|
|
10749
|
-
return
|
|
10814
|
+
return join10(tmpdir(), `nanopm-run-${jobId}`);
|
|
10750
10815
|
}
|
|
10751
10816
|
async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
10752
10817
|
const refuse2 = (why) => {
|
|
@@ -10771,7 +10836,7 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
|
|
|
10771
10836
|
await mkdir4(fallback, { recursive: true, mode: 448 });
|
|
10772
10837
|
return fallback;
|
|
10773
10838
|
}
|
|
10774
|
-
if (
|
|
10839
|
+
if (existsSync4(allowed)) return resolve4(allowed);
|
|
10775
10840
|
if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
|
|
10776
10841
|
await mkdir4(fallback, { recursive: true, mode: 448 });
|
|
10777
10842
|
return fallback;
|
|
@@ -10782,10 +10847,10 @@ ${body}
|
|
|
10782
10847
|
</${label}>`;
|
|
10783
10848
|
}
|
|
10784
10849
|
function harnessEnv(timeoutMs, source = process.env) {
|
|
10785
|
-
const home = source.HOME ??
|
|
10850
|
+
const home = source.HOME ?? homedir3();
|
|
10786
10851
|
const env = {
|
|
10787
10852
|
HOME: home,
|
|
10788
|
-
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
10853
|
+
PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join10(home, ".local", "bin")].join(":"),
|
|
10789
10854
|
MCP_TOOL_TIMEOUT: String(timeoutMs)
|
|
10790
10855
|
};
|
|
10791
10856
|
for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
|
|
@@ -10865,6 +10930,10 @@ async function runDaemon(options = {}) {
|
|
|
10865
10930
|
const startedAt = Date.now();
|
|
10866
10931
|
let lastDeployCheck = 0;
|
|
10867
10932
|
const DEPLOY_CHECK_MS = 30 * 6e4;
|
|
10933
|
+
const UPDATE_CHECK_MS = 60 * 6e4;
|
|
10934
|
+
const updatable = !options.once && installKind() === "installer";
|
|
10935
|
+
let lastUpdateCheck = Date.now() - UPDATE_CHECK_MS + 5 * 6e4;
|
|
10936
|
+
let restart = false;
|
|
10868
10937
|
const cloneless = await clonelessSkills().catch(() => []);
|
|
10869
10938
|
const staleAfter = process.env.NANOPM_STALE_DAEMON_AFTER ?? "2 minutes";
|
|
10870
10939
|
let testingConnections = false;
|
|
@@ -10924,6 +10993,21 @@ async function runDaemon(options = {}) {
|
|
|
10924
10993
|
if (check.queued) log(`deploy on ${check.projectName}: review queued (${check.reason})`);
|
|
10925
10994
|
}
|
|
10926
10995
|
}
|
|
10996
|
+
if (updatable && Date.now() - lastUpdateCheck > UPDATE_CHECK_MS) {
|
|
10997
|
+
lastUpdateCheck = Date.now();
|
|
10998
|
+
const current = options.version ?? "dev";
|
|
10999
|
+
const off = autoUpdateOff();
|
|
11000
|
+
const latest = off ? null : await latestVersion();
|
|
11001
|
+
if (shouldUpdate({ kind: "installer", off, quickBusy: quick?.busy() ?? false, latest, current })) {
|
|
11002
|
+
const done = await installVersion(latest);
|
|
11003
|
+
if (done.ok) {
|
|
11004
|
+
log(`updated to ${latest}; restarting on it`);
|
|
11005
|
+
restart = true;
|
|
11006
|
+
break;
|
|
11007
|
+
}
|
|
11008
|
+
log(`update to ${latest} failed: ${done.why}; staying on ${current}`);
|
|
11009
|
+
}
|
|
11010
|
+
}
|
|
10927
11011
|
await heartbeat(auth.client, daemonId);
|
|
10928
11012
|
await sleep(pollIntervalMs, void 0, { signal: options.signal }).catch(() => void 0);
|
|
10929
11013
|
}
|
|
@@ -10934,7 +11018,7 @@ async function runDaemon(options = {}) {
|
|
|
10934
11018
|
lock.release();
|
|
10935
11019
|
log(`daemon ${daemonId} offline`);
|
|
10936
11020
|
}
|
|
10937
|
-
return { daemonId, processed };
|
|
11021
|
+
return { daemonId, processed, restart };
|
|
10938
11022
|
}
|
|
10939
11023
|
async function chainAfter(auth, job, outcome, log) {
|
|
10940
11024
|
if (NOT_CHAINED_BY_DAEMON.includes(job.skill)) return;
|
|
@@ -10986,7 +11070,7 @@ function quickLane(ctx, daemonId, log, everyMs = QUICK_POLL_MS) {
|
|
|
10986
11070
|
});
|
|
10987
11071
|
}, everyMs);
|
|
10988
11072
|
timer.unref?.();
|
|
10989
|
-
return { stop: () => clearInterval(timer) };
|
|
11073
|
+
return { stop: () => clearInterval(timer), busy: () => busy };
|
|
10990
11074
|
}
|
|
10991
11075
|
async function parentOf(auth, id) {
|
|
10992
11076
|
const { data } = await auth.client.from("jobs").select("skill, params").eq("id", id).maybeSingle();
|
|
@@ -11031,7 +11115,7 @@ var heldInThisProcess = false;
|
|
|
11031
11115
|
function acquireLock() {
|
|
11032
11116
|
if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
|
|
11033
11117
|
mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
|
|
11034
|
-
const path =
|
|
11118
|
+
const path = join11(nanopmHome(), "daemon.pid");
|
|
11035
11119
|
for (; ; ) {
|
|
11036
11120
|
try {
|
|
11037
11121
|
const fd = openSync(path, "wx");
|
|
@@ -11118,13 +11202,13 @@ async function reclaimOwnJobs(client, daemonId) {
|
|
|
11118
11202
|
async function harnessPids(runDir) {
|
|
11119
11203
|
const dirs = [runDir];
|
|
11120
11204
|
try {
|
|
11121
|
-
for (const d of await readdir2(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(
|
|
11205
|
+
for (const d of await readdir2(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join11(runDir, d.name));
|
|
11122
11206
|
} catch {
|
|
11123
11207
|
}
|
|
11124
11208
|
const pids = [];
|
|
11125
11209
|
for (const dir of dirs) {
|
|
11126
11210
|
try {
|
|
11127
|
-
const pid = Number((await readFile7(
|
|
11211
|
+
const pid = Number((await readFile7(join11(dir, "harness.pid"), "utf8")).trim());
|
|
11128
11212
|
if (pid > 1) pids.push(pid);
|
|
11129
11213
|
} catch {
|
|
11130
11214
|
}
|
|
@@ -11132,17 +11216,17 @@ async function harnessPids(runDir) {
|
|
|
11132
11216
|
return pids;
|
|
11133
11217
|
}
|
|
11134
11218
|
async function killOrphanedHarness(jobId) {
|
|
11135
|
-
const runDir =
|
|
11219
|
+
const runDir = join11(nanopmHome(), "run", jobId);
|
|
11136
11220
|
for (const pid of await harnessPids(runDir)) {
|
|
11137
11221
|
try {
|
|
11138
11222
|
process.kill(pid, "SIGTERM");
|
|
11139
11223
|
} catch {
|
|
11140
11224
|
}
|
|
11141
11225
|
}
|
|
11142
|
-
await
|
|
11226
|
+
await rm4(runDir, { recursive: true, force: true });
|
|
11143
11227
|
}
|
|
11144
11228
|
async function registerDaemon(auth, version, opts = {}) {
|
|
11145
|
-
const path =
|
|
11229
|
+
const path = join11(nanopmHome(), "daemon.json");
|
|
11146
11230
|
let identity = null;
|
|
11147
11231
|
try {
|
|
11148
11232
|
identity = JSON.parse(await readFile7(path, "utf8"));
|
|
@@ -11184,7 +11268,7 @@ async function registerDaemon(auth, version, opts = {}) {
|
|
|
11184
11268
|
}
|
|
11185
11269
|
async function rememberDaemon(id, shared = false) {
|
|
11186
11270
|
await mkdir5(nanopmHome(), { recursive: true, mode: 448 });
|
|
11187
|
-
await
|
|
11271
|
+
await writeFile4(join11(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
|
|
11188
11272
|
}
|
|
11189
11273
|
async function heartbeat(client, daemonId) {
|
|
11190
11274
|
await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
|
|
@@ -11389,11 +11473,11 @@ function checkAgainstDecisions(item, prior, sourceDates) {
|
|
|
11389
11473
|
}
|
|
11390
11474
|
if (match.status === "rejected") {
|
|
11391
11475
|
if (!match.decidedAt) return `"${item.title}" was rejected; it comes back only with evidence written since`;
|
|
11392
|
-
const
|
|
11476
|
+
const newer2 = item.sources.some((s) => {
|
|
11393
11477
|
const at = sourceDates(s.ref);
|
|
11394
11478
|
return at !== void 0 && at > match.decidedAt;
|
|
11395
11479
|
});
|
|
11396
|
-
if (!
|
|
11480
|
+
if (!newer2) {
|
|
11397
11481
|
return `"${item.title}" was rejected on ${match.decidedAt.slice(0, 10)}. It comes back only citing something learned since, and saying what changed`;
|
|
11398
11482
|
}
|
|
11399
11483
|
}
|
|
@@ -11537,14 +11621,14 @@ function refuseFetch(url, allowlist, open = false) {
|
|
|
11537
11621
|
|
|
11538
11622
|
// ../memory-mcp/src/repo-rules.ts
|
|
11539
11623
|
import { lstat, unlink } from "fs/promises";
|
|
11540
|
-
import { relative as relative3, resolve as resolve5, sep } from "path";
|
|
11624
|
+
import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
|
|
11541
11625
|
var REMOVE_MAX = 50;
|
|
11542
11626
|
function refuseRemove(workspace, requested) {
|
|
11543
|
-
const
|
|
11544
|
-
const target = resolve5(
|
|
11545
|
-
const rel = relative3(
|
|
11546
|
-
if (!rel || rel.startsWith("..") || rel.startsWith(`..${
|
|
11547
|
-
if (rel === ".git" || rel.startsWith(`.git${
|
|
11627
|
+
const root2 = resolve5(workspace);
|
|
11628
|
+
const target = resolve5(root2, requested);
|
|
11629
|
+
const rel = relative3(root2, target);
|
|
11630
|
+
if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
|
|
11631
|
+
if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
|
|
11548
11632
|
if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
|
|
11549
11633
|
return null;
|
|
11550
11634
|
}
|
|
@@ -13150,7 +13234,7 @@ var MEMORY_TOOL_NAMES = [
|
|
|
13150
13234
|
"list_market",
|
|
13151
13235
|
"write_market",
|
|
13152
13236
|
"remove_files",
|
|
13153
|
-
// Talk to Nano (docs
|
|
13237
|
+
// Talk to Nano (docs/✅nanopm-68-talk-to-nano.md): the map read, and changed as the founder asked.
|
|
13154
13238
|
...TALK_TOOLS
|
|
13155
13239
|
];
|
|
13156
13240
|
async function startMemoryServer(env = process.env) {
|
|
@@ -13163,16 +13247,16 @@ async function startMemoryServer(env = process.env) {
|
|
|
13163
13247
|
import { readFile as readFile9 } from "fs/promises";
|
|
13164
13248
|
|
|
13165
13249
|
// src/project.ts
|
|
13166
|
-
import { execFile as
|
|
13250
|
+
import { execFile as execFile7 } from "child_process";
|
|
13167
13251
|
import { basename } from "path";
|
|
13168
|
-
import { promisify as
|
|
13252
|
+
import { promisify as promisify7 } from "util";
|
|
13169
13253
|
|
|
13170
13254
|
// src/local.ts
|
|
13171
13255
|
import { readFile as readFile8 } from "fs/promises";
|
|
13172
|
-
import { join as
|
|
13256
|
+
import { join as join12 } from "path";
|
|
13173
13257
|
async function localDaemonId() {
|
|
13174
13258
|
try {
|
|
13175
|
-
const parsed = JSON.parse(await readFile8(
|
|
13259
|
+
const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
|
|
13176
13260
|
return parsed.id ?? null;
|
|
13177
13261
|
} catch {
|
|
13178
13262
|
return null;
|
|
@@ -13183,30 +13267,30 @@ function useColor() {
|
|
|
13183
13267
|
}
|
|
13184
13268
|
|
|
13185
13269
|
// src/project.ts
|
|
13186
|
-
var
|
|
13270
|
+
var execFileAsync7 = promisify7(execFile7);
|
|
13187
13271
|
async function detectRepo(cwd) {
|
|
13188
|
-
let
|
|
13272
|
+
let root2;
|
|
13189
13273
|
try {
|
|
13190
|
-
|
|
13274
|
+
root2 = (await execFileAsync7("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13191
13275
|
} catch {
|
|
13192
13276
|
return null;
|
|
13193
13277
|
}
|
|
13194
13278
|
let remote = null;
|
|
13195
13279
|
try {
|
|
13196
|
-
remote = canonicalRemote((await
|
|
13280
|
+
remote = canonicalRemote((await execFileAsync7("git", ["-C", root2, "remote", "get-url", "origin"])).stdout.trim());
|
|
13197
13281
|
} catch {
|
|
13198
13282
|
remote = null;
|
|
13199
13283
|
}
|
|
13200
|
-
const name = remote ? remote.split("/").pop() : basename(
|
|
13201
|
-
return { root, remote, name, empty: await repoIsEmpty(
|
|
13284
|
+
const name = remote ? remote.split("/").pop() : basename(root2);
|
|
13285
|
+
return { root: root2, remote, name, empty: await repoIsEmpty(root2) };
|
|
13202
13286
|
}
|
|
13203
|
-
async function repoIsEmpty(
|
|
13287
|
+
async function repoIsEmpty(root2) {
|
|
13204
13288
|
try {
|
|
13205
|
-
await
|
|
13289
|
+
await execFileAsync7("git", ["-C", root2, "rev-parse", "--verify", "HEAD"]);
|
|
13206
13290
|
} catch {
|
|
13207
13291
|
return true;
|
|
13208
13292
|
}
|
|
13209
|
-
const files = (await
|
|
13293
|
+
const files = (await execFileAsync7("git", ["-C", root2, "ls-files"])).stdout.split("\n").filter(Boolean);
|
|
13210
13294
|
return files.every((f) => {
|
|
13211
13295
|
const base = basename(f);
|
|
13212
13296
|
return base.startsWith(".") || /^(readme|licen[cs]e)(\.|$)/i.test(base);
|
|
@@ -13875,17 +13959,17 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
|
|
|
13875
13959
|
}
|
|
13876
13960
|
|
|
13877
13961
|
// src/commands/daemon-service.ts
|
|
13878
|
-
import { execFile as
|
|
13879
|
-
import { existsSync as
|
|
13880
|
-
import { mkdir as mkdir6, readFile as readFile11, rm as
|
|
13881
|
-
import { homedir as
|
|
13882
|
-
import { join as
|
|
13883
|
-
import { promisify as
|
|
13962
|
+
import { execFile as execFile8 } from "child_process";
|
|
13963
|
+
import { existsSync as existsSync5 } from "fs";
|
|
13964
|
+
import { mkdir as mkdir6, readFile as readFile11, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
13965
|
+
import { homedir as homedir4 } from "os";
|
|
13966
|
+
import { join as join13 } from "path";
|
|
13967
|
+
import { promisify as promisify8 } from "util";
|
|
13884
13968
|
|
|
13885
13969
|
// package.json
|
|
13886
13970
|
var package_default = {
|
|
13887
13971
|
name: "@nanopm/cli",
|
|
13888
|
-
version: "0.1.
|
|
13972
|
+
version: "0.1.7",
|
|
13889
13973
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
13890
13974
|
type: "module",
|
|
13891
13975
|
license: "MIT",
|
|
@@ -13934,13 +14018,13 @@ var package_default = {
|
|
|
13934
14018
|
var VERSION = package_default.version;
|
|
13935
14019
|
|
|
13936
14020
|
// src/commands/daemon-service.ts
|
|
13937
|
-
var
|
|
14021
|
+
var execFileAsync8 = promisify8(execFile8);
|
|
13938
14022
|
var LAUNCHD_LABEL = "pm.nano.daemon";
|
|
13939
14023
|
function plistPath() {
|
|
13940
|
-
return
|
|
14024
|
+
return join13(homedir4(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
13941
14025
|
}
|
|
13942
14026
|
function daemonLogPath() {
|
|
13943
|
-
return
|
|
14027
|
+
return join13(nanopmHome(), "daemon.log");
|
|
13944
14028
|
}
|
|
13945
14029
|
function renderPlist(opts) {
|
|
13946
14030
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -13999,31 +14083,39 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
|
|
|
13999
14083
|
out(web.ok ? "Claude's own search answers; searches go through your subscription." : `Claude's search is not available on this machine (${web.message}); searches use DuckDuckGo.`);
|
|
14000
14084
|
const bin = process.argv[1];
|
|
14001
14085
|
if (!bin) throw new Error("cannot locate the nanopm binary");
|
|
14002
|
-
const home =
|
|
14003
|
-
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
14004
|
-
await mkdir6(
|
|
14086
|
+
const home = homedir4();
|
|
14087
|
+
const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":");
|
|
14088
|
+
await mkdir6(join13(home, "Library", "LaunchAgents"), { recursive: true });
|
|
14005
14089
|
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
14006
14090
|
if (opts.shared !== void 0) await rememberShared(opts.shared);
|
|
14007
14091
|
await found({ ...detection, authenticated: true });
|
|
14008
|
-
await
|
|
14009
|
-
const target =
|
|
14010
|
-
await
|
|
14011
|
-
await
|
|
14092
|
+
await writeFile5(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
|
|
14093
|
+
const target = guiTarget();
|
|
14094
|
+
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14095
|
+
await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
|
|
14012
14096
|
out(`Installed ${LAUNCHD_LABEL}; the daemon starts now and at every login. Logs: ${daemonLogPath()}`);
|
|
14013
14097
|
const shared = opts.shared ?? await localShared();
|
|
14014
14098
|
out(shared ? "This machine is shared with your projects: it also runs scheduled reviews and partners' jobs." : "This machine runs your jobs only. `nanopm daemon install --share` to offer it to your projects.");
|
|
14015
14099
|
if (opts.next !== false) out(`Next, in a repo: nanopm onboard \xB7 or continue at ${webUrl()}/new`);
|
|
14016
14100
|
}
|
|
14101
|
+
function guiTarget() {
|
|
14102
|
+
return `gui/${process.getuid?.() ?? 501}`;
|
|
14103
|
+
}
|
|
14104
|
+
async function restartDaemon() {
|
|
14105
|
+
if (!existsSync5(plistPath())) return false;
|
|
14106
|
+
await execFileAsync8("launchctl", ["kickstart", "-k", `${guiTarget()}/${LAUNCHD_LABEL}`]);
|
|
14107
|
+
return true;
|
|
14108
|
+
}
|
|
14017
14109
|
async function uninstallDaemon(out = (l) => console.log(l)) {
|
|
14018
14110
|
requireMac();
|
|
14019
|
-
const target =
|
|
14020
|
-
await
|
|
14021
|
-
await
|
|
14111
|
+
const target = guiTarget();
|
|
14112
|
+
await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
|
|
14113
|
+
await rm5(plistPath(), { force: true });
|
|
14022
14114
|
out(`Removed ${LAUNCHD_LABEL}.`);
|
|
14023
14115
|
}
|
|
14024
14116
|
async function daemonStatus(out = (l) => console.log(l)) {
|
|
14025
14117
|
const id = await localDaemonId();
|
|
14026
|
-
const installed = process.platform === "darwin" &&
|
|
14118
|
+
const installed = process.platform === "darwin" && existsSync5(plistPath());
|
|
14027
14119
|
out(`launchd: ${installed ? "installed" : "not installed"}`);
|
|
14028
14120
|
if (!id) {
|
|
14029
14121
|
out("daemon: never registered on this machine");
|
|
@@ -14053,14 +14145,14 @@ async function daemonLogs(lines = 50) {
|
|
|
14053
14145
|
}
|
|
14054
14146
|
async function localWebSearch() {
|
|
14055
14147
|
try {
|
|
14056
|
-
const parsed = JSON.parse(await readFile11(
|
|
14148
|
+
const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
|
|
14057
14149
|
return parsed.webSearch === true;
|
|
14058
14150
|
} catch {
|
|
14059
14151
|
return false;
|
|
14060
14152
|
}
|
|
14061
14153
|
}
|
|
14062
14154
|
async function rememberWebSearch(webSearch2) {
|
|
14063
|
-
const path =
|
|
14155
|
+
const path = join13(nanopmHome(), "daemon.json");
|
|
14064
14156
|
let parsed = {};
|
|
14065
14157
|
try {
|
|
14066
14158
|
parsed = JSON.parse(await readFile11(path, "utf8"));
|
|
@@ -14068,18 +14160,18 @@ async function rememberWebSearch(webSearch2) {
|
|
|
14068
14160
|
parsed = {};
|
|
14069
14161
|
}
|
|
14070
14162
|
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
14071
|
-
await
|
|
14163
|
+
await writeFile5(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
|
|
14072
14164
|
}
|
|
14073
14165
|
async function localShared() {
|
|
14074
14166
|
try {
|
|
14075
|
-
const parsed = JSON.parse(await readFile11(
|
|
14167
|
+
const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
|
|
14076
14168
|
return Boolean(parsed.shared);
|
|
14077
14169
|
} catch {
|
|
14078
14170
|
return false;
|
|
14079
14171
|
}
|
|
14080
14172
|
}
|
|
14081
14173
|
async function rememberShared(shared) {
|
|
14082
|
-
const path =
|
|
14174
|
+
const path = join13(nanopmHome(), "daemon.json");
|
|
14083
14175
|
let parsed = {};
|
|
14084
14176
|
try {
|
|
14085
14177
|
parsed = JSON.parse(await readFile11(path, "utf8"));
|
|
@@ -14087,7 +14179,7 @@ async function rememberShared(shared) {
|
|
|
14087
14179
|
parsed = {};
|
|
14088
14180
|
}
|
|
14089
14181
|
await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
|
|
14090
|
-
await
|
|
14182
|
+
await writeFile5(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
|
|
14091
14183
|
}
|
|
14092
14184
|
|
|
14093
14185
|
// src/format/log.ts
|
|
@@ -14215,11 +14307,11 @@ async function correctionNote(client, jobId) {
|
|
|
14215
14307
|
import { setTimeout as sleep4 } from "timers/promises";
|
|
14216
14308
|
|
|
14217
14309
|
// src/browser.ts
|
|
14218
|
-
import { execFile as
|
|
14310
|
+
import { execFile as execFile9 } from "child_process";
|
|
14219
14311
|
function openBrowser(url, interactive = Boolean(process.stdout.isTTY)) {
|
|
14220
14312
|
if (!interactive) return false;
|
|
14221
14313
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
14222
|
-
|
|
14314
|
+
execFile9(cmd, [url], () => void 0);
|
|
14223
14315
|
return true;
|
|
14224
14316
|
}
|
|
14225
14317
|
|
|
@@ -14303,6 +14395,33 @@ async function logout(out = (l) => console.log(l)) {
|
|
|
14303
14395
|
out("Logged out. The daemon on this machine will stop taking jobs at its next start.");
|
|
14304
14396
|
}
|
|
14305
14397
|
|
|
14398
|
+
// src/commands/update.ts
|
|
14399
|
+
async function update(opts, out = (l) => console.log(l)) {
|
|
14400
|
+
if (opts.auto !== void 0) {
|
|
14401
|
+
if (opts.auto !== "on" && opts.auto !== "off") throw new Error("--auto takes on or off");
|
|
14402
|
+
await setAutoUpdate(opts.auto === "on");
|
|
14403
|
+
out(opts.auto === "on" ? "Automatic updates are on: NanoPM updates itself when it is idle." : "Automatic updates are off. `nanopm update` still updates when you ask.");
|
|
14404
|
+
return;
|
|
14405
|
+
}
|
|
14406
|
+
if (installKind() !== "installer") {
|
|
14407
|
+
out(`This NanoPM (${VERSION}) was not installed with the install line, so it does not update itself.`);
|
|
14408
|
+
out("From a clone of nanopm2: git pull && pnpm build && nanopm daemon install");
|
|
14409
|
+
out("Installed with npm: npm i -g @nanopm/cli@latest && nanopm daemon install");
|
|
14410
|
+
return;
|
|
14411
|
+
}
|
|
14412
|
+
const latest = await latestVersion();
|
|
14413
|
+
if (!latest) throw new Error("could not reach the npm registry; try again in a moment");
|
|
14414
|
+
if (!newer(latest, VERSION)) {
|
|
14415
|
+
out(`Already up to date (${VERSION}).`);
|
|
14416
|
+
return;
|
|
14417
|
+
}
|
|
14418
|
+
out(`Updating ${VERSION} \u2192 ${latest}\u2026`);
|
|
14419
|
+
const done = await installVersion(latest);
|
|
14420
|
+
if (!done.ok) throw new Error(`${done.why}
|
|
14421
|
+
The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
|
|
14422
|
+
out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
|
|
14423
|
+
}
|
|
14424
|
+
|
|
14306
14425
|
// src/format/roadmap.ts
|
|
14307
14426
|
var SHAPE = {
|
|
14308
14427
|
proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
|
|
@@ -15292,7 +15411,7 @@ async function verifyCommand(command, opts) {
|
|
|
15292
15411
|
|
|
15293
15412
|
// src/detect-sources.ts
|
|
15294
15413
|
import { readFile as readFile12 } from "fs/promises";
|
|
15295
|
-
import { join as
|
|
15414
|
+
import { join as join14 } from "path";
|
|
15296
15415
|
var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
|
|
15297
15416
|
var POSTGRES_KEYS = [
|
|
15298
15417
|
"DATABASE_URL",
|
|
@@ -15342,7 +15461,7 @@ async function envFiles(repoRoot) {
|
|
|
15342
15461
|
const out = [];
|
|
15343
15462
|
for (const file of ENV_FILES) {
|
|
15344
15463
|
try {
|
|
15345
|
-
out.push({ file, env: parseEnv(await readFile12(
|
|
15464
|
+
out.push({ file, env: parseEnv(await readFile12(join14(repoRoot, file), "utf8")) });
|
|
15346
15465
|
} catch {
|
|
15347
15466
|
continue;
|
|
15348
15467
|
}
|
|
@@ -17083,7 +17202,7 @@ async function invite(email, opts) {
|
|
|
17083
17202
|
const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
|
|
17084
17203
|
return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
|
|
17085
17204
|
}
|
|
17086
|
-
async function
|
|
17205
|
+
async function join15(code, opts) {
|
|
17087
17206
|
const normalized2 = code.trim().toUpperCase();
|
|
17088
17207
|
if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
|
|
17089
17208
|
const repo = await detectRepo(opts.cwd);
|
|
@@ -17368,7 +17487,7 @@ async function showSchedule(opts) {
|
|
|
17368
17487
|
}
|
|
17369
17488
|
|
|
17370
17489
|
// src/commands/status.ts
|
|
17371
|
-
import { existsSync as
|
|
17490
|
+
import { existsSync as existsSync6 } from "fs";
|
|
17372
17491
|
async function status(opts) {
|
|
17373
17492
|
const auth = await authenticate();
|
|
17374
17493
|
const color = opts.color ?? useColor();
|
|
@@ -17421,7 +17540,7 @@ async function projectStatus(client, project) {
|
|
|
17421
17540
|
openProblems: (problems2.data ?? []).length,
|
|
17422
17541
|
unprovenProblems: (problems2.data ?? []).filter((p) => p.importance_basis === "judgment").length,
|
|
17423
17542
|
search: describeSearch(search, await localWebSearch()),
|
|
17424
|
-
onThisMachine: Boolean(project.repoPath &&
|
|
17543
|
+
onThisMachine: Boolean(project.repoPath && existsSync6(project.repoPath)),
|
|
17425
17544
|
measures: measures({
|
|
17426
17545
|
solutionStatuses: (roadmap2.data ?? []).map((r) => r.status),
|
|
17427
17546
|
decisions: decisions2.data ?? [],
|
|
@@ -17460,6 +17579,7 @@ function buildProgram() {
|
|
|
17460
17579
|
process.once("SIGINT", stop2);
|
|
17461
17580
|
process.once("SIGTERM", stop2);
|
|
17462
17581
|
const result = await runDaemon({ once: opts.once, version: VERSION, signal: abort.signal });
|
|
17582
|
+
if (result.restart) process.exit(0);
|
|
17463
17583
|
if (opts.once && result.processed === 0) {
|
|
17464
17584
|
console.error("nanopm: no queued job arrived in time");
|
|
17465
17585
|
process.exitCode = 2;
|
|
@@ -17469,6 +17589,7 @@ function buildProgram() {
|
|
|
17469
17589
|
daemon.command("uninstall").description("Remove the background daemon").action(async () => uninstallDaemon());
|
|
17470
17590
|
daemon.command("status").description("Is the daemon online, and what is queued").action(async () => daemonStatus());
|
|
17471
17591
|
daemon.command("logs").description("The daemon log").option("-n <lines>", "lines", (v) => Number(v), 50).action(async (opts) => console.log(await daemonLogs(opts.n)));
|
|
17592
|
+
program.command("update").description("Update NanoPM to the latest release now; it also updates itself when idle").option("--auto <on|off>", "turn the automatic update on or off").action(async (opts) => update(opts));
|
|
17472
17593
|
program.command("login").description("Connect this computer to your nano.pm account, and start the daemon on it").option("--no-browser", "print the link instead of opening it").option("--no-daemon", "link only; do not install the background daemon").option("--web <url>", "nano.pm base URL (development)").action(async (opts) => {
|
|
17473
17594
|
await login({ noBrowser: !opts.browser, noDaemon: !opts.daemon, web: opts.web });
|
|
17474
17595
|
});
|
|
@@ -17779,7 +17900,7 @@ function buildProgram() {
|
|
|
17779
17900
|
console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
|
17780
17901
|
});
|
|
17781
17902
|
program.command("join <code>").description("Join a project you were invited to, from inside your clone of its repository").option("--no-color", "plain output").action(async (code, opts) => {
|
|
17782
|
-
console.log(await
|
|
17903
|
+
console.log(await join15(code, { cwd: process.cwd(), color: colorOf(opts) }));
|
|
17783
17904
|
});
|
|
17784
17905
|
program.command("members").description("Who is on this project, and whether their daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
|
|
17785
17906
|
console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
|
package/package.json
CHANGED
|
@@ -16,5 +16,5 @@ A mission or a vision the founder has said yes to is theirs. When one exists, th
|
|
|
16
16
|
winner is written beside it as a proposal (`mission-next`, `vision-next`) and the founder
|
|
17
17
|
decides on the business card; the run never replaces a stated row.
|
|
18
18
|
|
|
19
|
-
See docs
|
|
19
|
+
See docs/✅nanopm-53-direction.md, and docs/reference/mission-vision.md for the rules the three
|
|
20
20
|
roles judge by.
|
package/skills/market/SKILL.md
CHANGED
|
@@ -16,4 +16,4 @@ what was learned *about* a company attached to that company rather than beside i
|
|
|
16
16
|
|
|
17
17
|
It replaces `research` everywhere a founder can see. `research` stays in the bundle for one
|
|
18
18
|
caller — the problem crew's inline look outside — and is chained from nowhere
|
|
19
|
-
(docs
|
|
19
|
+
(docs/✅nanopm-55-market-crew.md §9).
|
package/skills/needs/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
The needs crew's coordinator is **code**, not a model: `packages/daemon/src/needs-job.ts`.
|
|
2
2
|
This file exists because a job names a skill and a skill has a manifest.
|
|
3
3
|
|
|
4
|
-
Nothing reads this as a prompt. The run is five roles and a gate (docs
|
|
4
|
+
Nothing reads this as a prompt. The run is five roles and a gate (docs/✅nanopm-57-needs-map.md §4):
|
|
5
5
|
|
|
6
6
|
1. **`needs-listener`** — the product's own users: the signals, the repository's notes about
|
|
7
7
|
people, and on a deep run the connected services (`read_data`). No web, no write.
|
package/skills/onboard/SKILL.md
CHANGED
|
@@ -81,8 +81,9 @@ product is.
|
|
|
81
81
|
it does from the line alone? These three are the pitch the founder reads first on the
|
|
82
82
|
web and says yes to; the pitch crew may propose better ones later, beside theirs.
|
|
83
83
|
- Batch 1, who it is for: `segments` up to 3 — **personas**, see step 4 — and `needs` up
|
|
84
|
-
to 5, each keyed to the persona it belongs to; **`mission` and `vision
|
|
85
|
-
|
|
84
|
+
to 5, each keyed to the persona it belongs to; then **`mission` and `vision`, always** —
|
|
85
|
+
see step 4. They are climbed from the personas and needs, so they come right after them,
|
|
86
|
+
and Direction is the first section the founder reads after the pitch.
|
|
86
87
|
- Batch 2, the rest of what it is and the money: `identity` `what-it-is-for` and at
|
|
87
88
|
most one more, and `revenue` up to 4 (who pays, for what, how much).
|
|
88
89
|
- Batch 3, the product: `products` — the `areas` item, the `not-doing` item, plus up
|
|
@@ -163,18 +164,29 @@ product is.
|
|
|
163
164
|
roadmap, a milestone, a phase or a commit: those are things to build, not outcomes.
|
|
164
165
|
- a principle the code states in words ("no human in the loop", "never email
|
|
165
166
|
without consent") as `preferences`, one line each.
|
|
166
|
-
|
|
167
|
-
**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`observed`, with
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
167
|
+
A **mission** (topic `mission`, key `mission`) and a **vision** (topic `vision`, key
|
|
168
|
+
`vision`), **always**: Direction on the card is these two, Yes or Edit (Nicolas,
|
|
169
|
+
2026-10-02). Where the product's own words already say one — the first lines of a README,
|
|
170
|
+
a MANIFESTO, a landing page's hero, an About page, the founder's wiki — write those words,
|
|
171
|
+
`observed`, with the files as sources. Otherwise draft them, `inferred`:
|
|
172
|
+
- **Mission**: the long-term change the founder is making in a business situation, the
|
|
173
|
+
lasting problem they solve, for whom. For whom → what fundamental value → why it
|
|
174
|
+
matters, one sentence under 200 characters a person could repeat from memory.
|
|
175
|
+
- **Vision**: what becomes true for the world, or the industry, once the mission
|
|
176
|
+
succeeds — and a person still living in it, doing something. One or two sentences,
|
|
177
|
+
under 280.
|
|
178
|
+
**Climb to them from the personas and needs, never from the routes and the feature
|
|
179
|
+
set**: a mission read off what the code does is the pitch with a wider adjective
|
|
180
|
+
(docs/✅nanopm-53-direction.md §3). In your head, in order: what the primary persona does
|
|
181
|
+
today *instead* (usually not a product: the time goes and leaves nothing); why they
|
|
182
|
+
struggle, the cause and not the pain; the progress they need, true even if the product
|
|
183
|
+
were rebuilt in another form; their world once it worked. Then compress: the mission from
|
|
184
|
+
the progress, the vision from that world. Before writing, check both against the direction
|
|
185
|
+
crew's must-pass tests (docs/reference/mission-vision.md): neither is the product back —
|
|
186
|
+
no feature, area name, platform, metric or date; no competitor could sign either
|
|
187
|
+
unchanged; the vision is more than today slightly improved, an outcome rather than one
|
|
188
|
+
solution, and adds a picture the mission does not already hold. A stated mission or
|
|
189
|
+
vision is the founder's: propose nothing under its key.
|
|
178
190
|
Only when the repo states a stance in words (a README that says "maintenance mode",
|
|
179
191
|
a roadmap that says "growth"), call set_preferences with `stance_provenance:
|
|
180
192
|
"inferred"`; otherwise do not call it at all. The stance is `grow` until someone says
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
The opportunity crew's coordinator is **code**, not a model: `packages/daemon/src/opportunity-job.ts`.
|
|
2
2
|
This file exists because a job names a skill and a skill has a manifest.
|
|
3
3
|
|
|
4
|
-
Nothing reads this as a prompt. The run (docs
|
|
4
|
+
Nothing reads this as a prompt. The run (docs/✅nanopm-61-opportunity-tree-whole.md §29):
|
|
5
5
|
|
|
6
6
|
**Quick** — the onboarding's run, and a quick *Update the map*:
|
|
7
7
|
|
package/skills/personas/SKILL.md
CHANGED
|
@@ -13,4 +13,4 @@ not write where decisions are made. The crew reads the market view instead — w
|
|
|
13
13
|
already brought back — which is how a competitor's own words about their buyer reach the
|
|
14
14
|
personas without that boundary moving.
|
|
15
15
|
|
|
16
|
-
See docs
|
|
16
|
+
See docs/✅nanopm-50-personas.md.
|
package/skills/pitch/SKILL.md
CHANGED
|
@@ -13,4 +13,4 @@ A pitch the founder has said yes to is theirs. When one exists, the crew's winne
|
|
|
13
13
|
beside it as a proposal (`pitch-<line>-next`) and the founder decides on the business card;
|
|
14
14
|
the run never replaces a stated line.
|
|
15
15
|
|
|
16
|
-
See docs
|
|
16
|
+
See docs/✅nanopm-52-pitch-crew.md.
|
|
@@ -48,7 +48,7 @@ source merely to fill the sources array. Expert hypotheses may have an empty arr
|
|
|
48
48
|
A source whose `provenance` is `inferred` may be marked context or contradicts, never
|
|
49
49
|
supports: the store refuses it, and the refusal aborts the whole publication, so one
|
|
50
50
|
wrong mark costs the entire run. Since the market crew began keeping what it found but
|
|
51
|
-
could not read (docs
|
|
51
|
+
could not read (docs/✅nanopm-55-market-crew.md §5), the competitors it saw named in a
|
|
52
52
|
search and never opened arrive here as `inferred` — they are real names worth reasoning
|
|
53
53
|
from and they are not proof of anything.
|
|
54
54
|
|
package/skills/talk/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talk",
|
|
3
|
-
"description": "A conversation with Nano about one opportunity (docs
|
|
3
|
+
"description": "A conversation with Nano about one opportunity (docs/✅nanopm-68-talk-to-nano.md): one round per message, on the daemon's quick lane. Nano answers like a CPO on the founder's side — the objective first, one push back, then their call — and changes the map with his tools: what the founder asked for at once, his own ideas only as suggestions.",
|
|
4
4
|
"capabilities": ["talk"],
|
|
5
5
|
"model": "claude-sonnet-5-5",
|
|
6
6
|
"maxTurns": 12,
|