nikos 0.0.5 → 0.0.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/cli.js +4 -0
- package/dist/commands/video.js +116 -0
- package/docs/install.md +14 -0
- package/package.json +3 -3
- package/skills/nikos-mode/playbooks/index.md +1 -0
- package/skills/nikos-mode/playbooks/video.md +14 -0
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { runBet } from "./commands/bet.js";
|
|
|
11
11
|
import { runShow } from "./commands/show.js";
|
|
12
12
|
import { runDecisions } from "./commands/decisions.js";
|
|
13
13
|
import { runStory } from "./commands/story.js";
|
|
14
|
+
import { runVideo } from "./commands/video.js";
|
|
14
15
|
const commands = {
|
|
15
16
|
init: runInit,
|
|
16
17
|
validate: runValidate,
|
|
@@ -24,6 +25,7 @@ const commands = {
|
|
|
24
25
|
show: runShow,
|
|
25
26
|
decisions: runDecisions,
|
|
26
27
|
story: runStory,
|
|
28
|
+
video: runVideo,
|
|
27
29
|
};
|
|
28
30
|
const help = `nikos — spec-driven development for AI coding agents
|
|
29
31
|
|
|
@@ -46,6 +48,8 @@ Commands:
|
|
|
46
48
|
(--why, --story, --category, --supersedes)
|
|
47
49
|
bet new <bet-id> Write a time-boxed bet card linked to its stories
|
|
48
50
|
(--hypothesis, --kr, --uplift, --time-box, --story, --blocker)
|
|
51
|
+
video Draft a video brief and storyboard from the done stories
|
|
52
|
+
(--out, default marketing/storymap-video)
|
|
49
53
|
|
|
50
54
|
Docs: https://nikos.tools`;
|
|
51
55
|
async function main(argv) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { findIntentDir, featureStoryIds, read, walk } from "../lib/project.js";
|
|
5
|
+
import { parseFeature, scenariosOf } from "../lint/gherkin.js";
|
|
6
|
+
// Timing: a title card, then each beat holds longer for more stories, then the close.
|
|
7
|
+
const INTRO = 4.5, BEAT = 2.6, PER_STORY = 1.8, CLOSE = 6;
|
|
8
|
+
/** The constitution's front matter and its "## Section" bodies. */
|
|
9
|
+
function constitution(intentDir) {
|
|
10
|
+
const path = join(intentDir, "constitution.md");
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
throw new Error("No intent/constitution.md: run the idea-to-blueprint playbook first");
|
|
13
|
+
const text = read(path);
|
|
14
|
+
const fm = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
15
|
+
const meta = (fm ? parse(fm[1]) : {});
|
|
16
|
+
const sections = {};
|
|
17
|
+
for (const m of text.slice(fm?.[0].length ?? 0).matchAll(/^## (.+)\n([\s\S]*?)(?=^## |(?![\s\S]))/gm))
|
|
18
|
+
sections[m[1].trim()] = m[2].trim();
|
|
19
|
+
return { name: meta.name ?? "", idea: meta.idea ?? "", users: sections.Users ?? "", problem: sections.Problem ?? "" };
|
|
20
|
+
}
|
|
21
|
+
/** Draft a video storyboard from intent/: only stories marked done, one beat per activity. */
|
|
22
|
+
export function storyboard(dir) {
|
|
23
|
+
const intentDir = findIntentDir(dir);
|
|
24
|
+
const c = constitution(intentDir);
|
|
25
|
+
const map = parse(read(join(intentDir, "storymap.yaml")));
|
|
26
|
+
const state = (parse(read(join(intentDir, "state.yaml"))) ?? {});
|
|
27
|
+
const stories = walk(join(intentDir, "features"), ".feature").flatMap((path) => {
|
|
28
|
+
const text = read(path);
|
|
29
|
+
const id = featureStoryIds(text)[0];
|
|
30
|
+
const feature = parseFeature(text);
|
|
31
|
+
if (!id || !feature)
|
|
32
|
+
return [];
|
|
33
|
+
const activity = feature.tags.map((t) => t.name).find((t) => t.startsWith("@activity-"))?.slice(10) ?? "";
|
|
34
|
+
// "AC1 — A new plant shows its next watering day" → "A new plant shows its next watering day"
|
|
35
|
+
const outcomes = scenariosOf(feature).map((s) => s.name.replace(/^AC\d+\s*[—–-]\s*/, ""));
|
|
36
|
+
return [{ id, title: feature.name, activity, outcomes, done: state.stories?.[id]?.status === "done" }];
|
|
37
|
+
});
|
|
38
|
+
const byNumber = (a, b) => Number(a.id.split("-")[1]) - Number(b.id.split("-")[1]);
|
|
39
|
+
const done = stories.filter((s) => s.done).sort(byNumber);
|
|
40
|
+
if (!done.length)
|
|
41
|
+
throw new Error("No story is done yet. A video shows only what's built: finish a story first (nikos next)");
|
|
42
|
+
let t = INTRO;
|
|
43
|
+
const beats = (map.activities ?? []).flatMap((a) => {
|
|
44
|
+
const shown = done.filter((s) => s.activity === a.id).map(({ id, title, outcomes }) => ({ id, title, outcomes }));
|
|
45
|
+
if (!shown.length)
|
|
46
|
+
return [];
|
|
47
|
+
const from = t;
|
|
48
|
+
t += BEAT + PER_STORY * Math.min(shown.length, 4);
|
|
49
|
+
return [{ activity: a.title ?? a.id, from, to: t, stories: shown }];
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
product: { name: c.name, idea: c.idea },
|
|
53
|
+
users: c.users,
|
|
54
|
+
problem: c.problem,
|
|
55
|
+
beats,
|
|
56
|
+
notBuilt: stories.filter((s) => !s.done).sort(byNumber).map(({ id, title }) => ({ id, title })),
|
|
57
|
+
intro: [0, INTRO],
|
|
58
|
+
close: [t, t + CLOSE],
|
|
59
|
+
total: Math.ceil(t + CLOSE),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const sec = (n) => `${Math.round(n * 10) / 10}s`;
|
|
63
|
+
/** The brief a person approves before anything renders. */
|
|
64
|
+
export function briefOf(sb) {
|
|
65
|
+
const rows = sb.beats.map((b, i) => `| ${sec(b.from)}–${sec(b.to)} | ${String(i + 1).padStart(2, "0")} ${b.activity} | ${b.stories.map((s) => `${s.id} ${s.title}`).join("; ")} |`);
|
|
66
|
+
return [
|
|
67
|
+
`# Brief: ${sb.product.name}`,
|
|
68
|
+
"",
|
|
69
|
+
"Drafted by `nikos video` from intent/. Approve the beats before rendering; change anything here first.",
|
|
70
|
+
"",
|
|
71
|
+
`- **Product:** ${sb.product.name}. ${sb.product.idea}`,
|
|
72
|
+
`- **For:** ${sb.users}`,
|
|
73
|
+
`- **Problem:** ${sb.problem}`,
|
|
74
|
+
`- **Length:** about ${sb.total} seconds, 1080p, music only. Plays muted, so the labels carry the story.`,
|
|
75
|
+
"- **In:** only stories marked done in state.yaml, each with the outcomes its passing scenarios check.",
|
|
76
|
+
`- **Out:** ${sb.notBuilt.length ? `anything not built yet: ${sb.notBuilt.map((s) => `${s.id} ${s.title}`).join("; ")}.` : "nothing; every story is done."}`,
|
|
77
|
+
`- **Close:** ${sb.product.name}, and its idea in one line.`,
|
|
78
|
+
"",
|
|
79
|
+
"## Beats",
|
|
80
|
+
"",
|
|
81
|
+
"One beat per activity with done stories.",
|
|
82
|
+
"",
|
|
83
|
+
"| Time | Label | Stories shown |",
|
|
84
|
+
"| --- | --- | --- |",
|
|
85
|
+
`| ${sec(sb.intro[0])}–${sec(sb.intro[1])} | Title | ${sb.product.name}: ${sb.product.idea} |`,
|
|
86
|
+
...rows,
|
|
87
|
+
`| ${sec(sb.close[0])}–${sec(sb.close[1])} | Close | ${sb.product.name} |`,
|
|
88
|
+
"",
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
/** Write brief.md and storyboard.js (read by the Video pack's storyboard template) into `out`. */
|
|
92
|
+
export function writeVideo(dir, out) {
|
|
93
|
+
const sb = storyboard(dir);
|
|
94
|
+
mkdirSync(out, { recursive: true });
|
|
95
|
+
writeFileSync(join(out, "brief.md"), briefOf(sb));
|
|
96
|
+
writeFileSync(join(out, "storyboard.js"), `// Drafted by nikos video from intent/. The storyboard template reads it.\nwindow.STORYBOARD = ${JSON.stringify(sb, null, 2)};\n`);
|
|
97
|
+
return sb;
|
|
98
|
+
}
|
|
99
|
+
export function runVideo(args) {
|
|
100
|
+
const i = args.indexOf("--out");
|
|
101
|
+
const out = resolve(i >= 0 && args[i + 1] ? args[i + 1] : "marketing/storymap-video");
|
|
102
|
+
try {
|
|
103
|
+
const sb = writeVideo(process.cwd(), out);
|
|
104
|
+
const shown = sb.beats.reduce((n, b) => n + b.stories.length, 0);
|
|
105
|
+
const r = relative(process.cwd(), out), where = !r ? "." : r.startsWith("..") ? out : r;
|
|
106
|
+
console.log(`Wrote ${where}/brief.md and ${where}/storyboard.js: ${sb.beats.length} beat${sb.beats.length === 1 ? "" : "s"}, ${shown} done ${shown === 1 ? "story" : "stories"}, about ${sb.total}s`);
|
|
107
|
+
if (sb.notBuilt.length)
|
|
108
|
+
console.log(`Left out, not done yet: ${sb.notBuilt.map((s) => s.id).join(", ")}`);
|
|
109
|
+
console.log("\nNext: approve the beats in brief.md, then render them with the Video pack's storyboard template.");
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error(`✗ ${err.message}`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
package/docs/install.md
CHANGED
|
@@ -50,6 +50,20 @@ then Add Marketplace, then Import from Repo), and install nikOS from it.
|
|
|
50
50
|
To try it without a marketplace, copy the repo to
|
|
51
51
|
`~/.cursor/plugins/local/nikos` and run "Developer: Reload Window".
|
|
52
52
|
|
|
53
|
+
## Packs
|
|
54
|
+
|
|
55
|
+
Packs are extra tools that install on their own, next to nikOS or without it.
|
|
56
|
+
Each lives in `tools/<pack>/` with its own manifests and skills, and both
|
|
57
|
+
marketplaces list it.
|
|
58
|
+
|
|
59
|
+
| Pack | Folder | Claude Code |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| Video | `tools/video/` | `/plugin install nikos-video@nikos` |
|
|
62
|
+
|
|
63
|
+
In Codex and Cursor, install the pack from the same marketplace as nikOS.
|
|
64
|
+
nikOS's router hands requests to an installed pack's skill, and gives the
|
|
65
|
+
install command when the pack is missing.
|
|
66
|
+
|
|
53
67
|
## The command-line tool
|
|
54
68
|
|
|
55
69
|
The skills run the `nikos` command. When it isn't installed, they run it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nikos",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Spec-driven development for AI coding agents: story maps, Gherkin acceptance criteria and a done gate for Claude Code, Codex and Cursor",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Niko Vijay",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"vitest": "^2.1.9"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@cucumber/gherkin": "^
|
|
42
|
-
"@cucumber/messages": "^
|
|
41
|
+
"@cucumber/gherkin": "^42.0.1",
|
|
42
|
+
"@cucumber/messages": "^34.2.1",
|
|
43
43
|
"ajv": "^8.17.1",
|
|
44
44
|
"yaml": "^2.6.1"
|
|
45
45
|
},
|
|
@@ -6,6 +6,7 @@ first match. Packs add rows here as they ship new playbooks.
|
|
|
6
6
|
| Playbook | File | Use when |
|
|
7
7
|
| --- | --- | --- |
|
|
8
8
|
| idea-to-blueprint | `idea-to-blueprint.md` | The person has a new product idea, or wants to start a project from scratch and there is no story map yet. |
|
|
9
|
+
| video | `video.md` | The person asks for a video: an explainer, product tour, demo, launch or social clip, or to edit or re-render one. Needs the Video pack. |
|
|
9
10
|
| validate | `validate.md` | The person wants to test or validate an idea with real people, or draft outreach, a post or a demo script for it. |
|
|
10
11
|
| bet | `bet.md` | The person wants to frame work as a bet, set a key result or time box for some stories, or close a bet and call its outcome. |
|
|
11
12
|
| build-a-story | `build-a-story.md` | The person asks to build, do or pick up the next story, or a story by its id. |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# video
|
|
2
|
+
|
|
3
|
+
Make a short product explainer video from a brief. The work is done by the
|
|
4
|
+
`product-tour-video` skill, which ships in the Video pack, not in nikOS
|
|
5
|
+
itself. This playbook checks the pack is there, pins down the brief, and
|
|
6
|
+
hands over to that skill's workflow.
|
|
7
|
+
|
|
8
|
+
1. Check the Video pack is installed: the `product-tour-video` skill is in your skills list. If it isn't, tell the person to install it (Claude Code: `/plugin install nikos-video@nikos`; Codex and Cursor: install `nikos-video` from the nikOS marketplace), say the video can't be made until they do, and stop.
|
|
9
|
+
2. Write the brief down before anything else. If the repo has an `intent/` folder with done stories, run `nikos video --out marketing/<name>`: it drafts `brief.md` and `storyboard.js` from `intent/constitution.md` and the stories marked done, one beat per activity, and leaves out everything not built yet. Otherwise write the product, who the video is for, its length, where it will live, the story it tells in one line, and what's in or out. Ask for anything missing.
|
|
10
|
+
3. Make it with the `product-tour-video` skill. From a drafted storyboard, render its storyboard template (the skill's "From a nikOS story map" section). For a tour of the product's real screens, follow the skill's workflow, every step in its order: find out what's real, storyboard for sign-off, the style test, the full build, the contact sheet, the score, then render and mix.
|
|
11
|
+
4. STOP at the storyboard (the beats in `brief.md`, or the skill's storyboard table) and, for a screen tour, again at the style test, and wait for the person's yes each time. A yes to one isn't a yes to the other.
|
|
12
|
+
5. Check the encoded `final.mp4`: extract a few frames with ffmpeg and look at them, and give its length and size.
|
|
13
|
+
6. Deliver `final.mp4` with the brief it was made from. Commit the video's sources only; the rendered files stay ignored.
|
|
14
|
+
7. Report every step with its evidence or its skip reason.
|