threadit-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/install.sh +16 -0
- package/assets/pack/capture-directive.md +1 -0
- package/assets/skills/threadit-orient/SKILL.md +15 -0
- package/assets/skills/threadit-reconcile/SKILL.md +15 -0
- package/dist/assets.d.ts +3 -0
- package/dist/assets.js +16 -0
- package/dist/bin.d.ts +11 -0
- package/dist/bin.js +472 -0
- package/dist/commands/accept.d.ts +12 -0
- package/dist/commands/accept.js +21 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +19 -0
- package/dist/commands/capture.d.ts +24 -0
- package/dist/commands/capture.js +45 -0
- package/dist/commands/doc.d.ts +7 -0
- package/dist/commands/doc.js +56 -0
- package/dist/commands/drop.d.ts +4 -0
- package/dist/commands/drop.js +12 -0
- package/dist/commands/fold.d.ts +1 -0
- package/dist/commands/fold.js +9 -0
- package/dist/commands/hook.d.ts +24 -0
- package/dist/commands/hook.js +52 -0
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.js +55 -0
- package/dist/commands/ls.d.ts +8 -0
- package/dist/commands/ls.js +27 -0
- package/dist/commands/merge.d.ts +4 -0
- package/dist/commands/merge.js +18 -0
- package/dist/commands/move.d.ts +11 -0
- package/dist/commands/move.js +45 -0
- package/dist/commands/pack.d.ts +5 -0
- package/dist/commands/pack.js +12 -0
- package/dist/commands/park.d.ts +2 -0
- package/dist/commands/park.js +10 -0
- package/dist/commands/promote.d.ts +11 -0
- package/dist/commands/promote.js +33 -0
- package/dist/commands/reconcile.d.ts +12 -0
- package/dist/commands/reconcile.js +97 -0
- package/dist/commands/serve.d.ts +17 -0
- package/dist/commands/serve.js +39 -0
- package/dist/commands/ship.d.ts +9 -0
- package/dist/commands/ship.js +28 -0
- package/dist/commands/show.d.ts +2 -0
- package/dist/commands/show.js +16 -0
- package/dist/commands/snooze.d.ts +5 -0
- package/dist/commands/snooze.js +16 -0
- package/dist/commands/status.d.ts +14 -0
- package/dist/commands/status.js +47 -0
- package/dist/commands/supersede.d.ts +1 -0
- package/dist/commands/supersede.js +9 -0
- package/dist/commands/sync.d.ts +16 -0
- package/dist/commands/sync.js +66 -0
- package/dist/commands/update.d.ts +13 -0
- package/dist/commands/update.js +23 -0
- package/dist/commands/validate.d.ts +18 -0
- package/dist/commands/validate.js +52 -0
- package/dist/commands/wrapup.d.ts +3 -0
- package/dist/commands/wrapup.js +10 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +20 -0
- package/dist/draft.d.ts +29 -0
- package/dist/draft.js +59 -0
- package/dist/git/commitFacts.d.ts +6 -0
- package/dist/git/commitFacts.js +56 -0
- package/dist/git/log.d.ts +41 -0
- package/dist/git/log.js +143 -0
- package/dist/git/reconcileCommits.d.ts +3 -0
- package/dist/git/reconcileCommits.js +18 -0
- package/dist/git/run.d.ts +2 -0
- package/dist/git/run.js +8 -0
- package/dist/ids.d.ts +4 -0
- package/dist/ids.js +25 -0
- package/dist/inbox/recurrence.d.ts +7 -0
- package/dist/inbox/recurrence.js +51 -0
- package/dist/install/atomicWrite.d.ts +2 -0
- package/dist/install/atomicWrite.js +9 -0
- package/dist/install/gitHooks.d.ts +6 -0
- package/dist/install/gitHooks.js +53 -0
- package/dist/install/gitignore.d.ts +4 -0
- package/dist/install/gitignore.js +29 -0
- package/dist/install/settings.d.ts +7 -0
- package/dist/install/settings.js +40 -0
- package/dist/install/skills.d.ts +11 -0
- package/dist/install/skills.js +38 -0
- package/dist/install/tty.d.ts +6 -0
- package/dist/install/tty.js +39 -0
- package/dist/mutate.d.ts +6 -0
- package/dist/mutate.js +21 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +46 -0
- package/dist/reconcileFormat.d.ts +3 -0
- package/dist/reconcileFormat.js +30 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +11 -0
- package/dist/transport/sync.d.ts +38 -0
- package/dist/transport/sync.js +64 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +34 -0
- package/dist/yaml/emit.d.ts +3 -0
- package/dist/yaml/emit.js +50 -0
- package/dist/yaml/io.d.ts +11 -0
- package/dist/yaml/io.js +33 -0
- package/package.json +32 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Threadit one-paste installer (reference). The server serves a token-injected variant.
|
|
3
|
+
# Usage (server-minted): curl -fsSL https://<server>/install/<token> | sh
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
: "${THREADIT_SERVER:?set THREADIT_SERVER}"
|
|
7
|
+
: "${THREADIT_TOKEN:?set THREADIT_TOKEN}"
|
|
8
|
+
: "${THREADIT_PROJECT:?set THREADIT_PROJECT}"
|
|
9
|
+
|
|
10
|
+
# 1. Install the client (pure-JS; no native build).
|
|
11
|
+
npm install -g threadit
|
|
12
|
+
|
|
13
|
+
# 2. Scaffold this repo. Token rides in via env (never on the command line → not in ps).
|
|
14
|
+
THREADIT_TOKEN="$THREADIT_TOKEN" threadit init "$THREADIT_PROJECT" \
|
|
15
|
+
--server "$THREADIT_SERVER" --project "$THREADIT_PROJECT"
|
|
16
|
+
# (skills location is prompted from /dev/tty; pass --skills global|repo to skip the prompt)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Capture discipline: the instant an aside, TODO, loop, or "we should…" surfaces — yours or the user's — run `threadit capture '<text>'` immediately. Don't defer it to wrap-up.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: threadit-orient
|
|
3
|
+
description: Use at the start of a session in a Threadit repo (a repo with threadit.yml) to orient — load the live work state, surface findings on entry, re-ground stale summaries, and decide what to read first. Triggers on "orient", "where are we", "start of session", "what's the state of this project".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Threadit — Orient (session start)
|
|
7
|
+
|
|
8
|
+
The session-start ritual. Ground against ground truth, not your compacted memory.
|
|
9
|
+
|
|
10
|
+
1. **Load state.** Run `threadit status`. It prints the working slice, findings, and the reading list.
|
|
11
|
+
2. **Surface findings on entry.** Read every finding aloud-to-self. A `unbacked-shipped`, `precondition-violation`, `stale-summary`, or `severity-review` means the record drifted from reality — flag it now, not 25 sessions later. Do not silently proceed past a finding.
|
|
12
|
+
3. **Re-ground stale summaries.** For each `stale-summary` finding, the node's summary predates its latest work. Re-derive it: read the git + linked-doc delta since the summary's `summary_at` sha, then **propose** a rewritten summary to the user (do not auto-apply). This breaks the stale-begets-staler loop.
|
|
13
|
+
4. **Read in order.** Present the reading list and state what to read first. Read those docs before acting.
|
|
14
|
+
|
|
15
|
+
You author judgment (interpretation + the re-grounded summary prose); the CLI does the mechanics (`threadit status`). Never invent state — if `status` and your memory disagree, `status` wins.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: threadit-reconcile
|
|
3
|
+
description: Use at the end of a session in a Threadit repo (a repo with threadit.yml) to wrap up — triage the inbox, reconcile claimed-vs-committed status, surface un-captured loops, and drain the inbox with forced dispositions. Triggers on "wrap up", "reconcile", "end of session", "before we stop".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Threadit — Reconcile (wrap-up)
|
|
7
|
+
|
|
8
|
+
The wrap-up honesty pass. Every surfaced item gets a disposition — no "ignore."
|
|
9
|
+
|
|
10
|
+
1. **Compute proposals.** Run `threadit reconcile`. It prints a grouped proposal list: inbox dispositions (promote/merge/drop/snooze), status proposals (e.g. resummarize a stale summary, downgrade an unbacked ship), and un-captured loops.
|
|
11
|
+
2. **Batch-approve with per-item veto.** Walk the list with the user. Every inbox item must get promote/merge/drop/snooze — the drain is forced; nothing may be left "open." Veto or adjust any single proposal.
|
|
12
|
+
3. **Author the prose the CLI won't.** The CLI applies prose but never writes it. For an accepted `resummarize`, write the new summary text. For an `accept` (muting a review-class finding), write the rationale and the `until` trigger. Hand these to the apply step.
|
|
13
|
+
4. **Apply atomically.** Build the disposition map and run `threadit reconcile --apply '<json>'`. This executes the two-pass atomic apply and stamps the `reconcile.sha` watermark.
|
|
14
|
+
|
|
15
|
+
You author judgment + prose; the CLI executes mechanics. This is the step the global wrap-up skill will eventually call instead of hand-maintaining a threads file.
|
package/dist/assets.d.ts
ADDED
package/dist/assets.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/** Absolute path to the bundled assets dir (package-root `assets/`, shipped via package.json "files"). */
|
|
4
|
+
function assetsRoot() {
|
|
5
|
+
// dist/assets.js → ../assets
|
|
6
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', 'assets');
|
|
7
|
+
}
|
|
8
|
+
export function captureDirectivePath() {
|
|
9
|
+
return join(assetsRoot(), 'pack', 'capture-directive.md');
|
|
10
|
+
}
|
|
11
|
+
export function skillSourceDir() {
|
|
12
|
+
return join(assetsRoot(), 'skills');
|
|
13
|
+
}
|
|
14
|
+
export function installScriptPath() {
|
|
15
|
+
return join(assetsRoot(), 'install.sh');
|
|
16
|
+
}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
/** Build the full commander program. Exported for tests; bin invocation calls parseAsync below. */
|
|
4
|
+
export declare function buildProgram(): Command;
|
|
5
|
+
/**
|
|
6
|
+
* True iff this module is the process entrypoint. Robust to the npm-bin symlink case:
|
|
7
|
+
* when installed, the binary is a symlink (e.g. `.../bin/threadit`) whose name is NOT `bin.js`,
|
|
8
|
+
* so the old `argv[1].endsWith('bin.js')` heuristic silently no-op'd the entire CLI. We resolve
|
|
9
|
+
* argv[1]'s realpath (following the symlink) and compare it to this module's own path.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isBinInvocation(argv1: string | undefined, moduleHref: string): boolean;
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolveRepoRoot } from './paths.js';
|
|
6
|
+
import { shouldPushAfter, maybePushDraft, defaultDraftPushDeps, shouldPushInboxAfter, maybePushInbox, defaultInboxPushDeps } from './draft.js';
|
|
7
|
+
import { PACK_VERSION } from './version.js';
|
|
8
|
+
function parseIntOpt(v) { return Number.parseInt(v, 10); }
|
|
9
|
+
async function readStdin() {
|
|
10
|
+
if (process.stdin.isTTY)
|
|
11
|
+
return '';
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for await (const c of process.stdin)
|
|
14
|
+
chunks.push(c);
|
|
15
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
16
|
+
}
|
|
17
|
+
/** Build the full commander program. Exported for tests; bin invocation calls parseAsync below. */
|
|
18
|
+
export function buildProgram() {
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name('threadit')
|
|
22
|
+
.description('Threadit — repo-side CLI for the work-structure visualizer')
|
|
23
|
+
.version(PACK_VERSION, '-v, --version', 'print the Threadit CLI version')
|
|
24
|
+
.option('--repo <dir>', 'repo root (default: walk up from cwd to find threadit.yml or .git)')
|
|
25
|
+
.option('--json', 'machine-readable JSON output where supported');
|
|
26
|
+
// --- integrity ---
|
|
27
|
+
program
|
|
28
|
+
.command('validate')
|
|
29
|
+
.argument('[path]', 'path to threadit.yml (default: resolved threadit.yml)')
|
|
30
|
+
.description('structural gate — exit non-zero on structural errors')
|
|
31
|
+
.action(async (path) => {
|
|
32
|
+
const { runValidateCommand } = await import('./commands/validate.js');
|
|
33
|
+
const { code, output } = await runValidateCommand(program.opts(), path);
|
|
34
|
+
if (output)
|
|
35
|
+
(code === 0 ? process.stdout : process.stderr).write(output + '\n');
|
|
36
|
+
process.exit(code);
|
|
37
|
+
});
|
|
38
|
+
const hookCmd = program.command('hook').description('internal hook entrypoints (called from .claude/settings.json)');
|
|
39
|
+
hookCmd
|
|
40
|
+
.command('capture-directive')
|
|
41
|
+
.description('emit the SessionStart capture directive as additionalContext')
|
|
42
|
+
.action(async () => {
|
|
43
|
+
const { runHookCaptureDirective } = await import('./commands/hook.js');
|
|
44
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
45
|
+
process.stdout.write(runHookCaptureDirective(resolveRepoRoot(process.cwd(), program.opts().repo)) + '\n');
|
|
46
|
+
});
|
|
47
|
+
hookCmd
|
|
48
|
+
.command('validate')
|
|
49
|
+
.description('PostToolUse: validate threadit.yml after an edit (warn-only; reads payload on stdin)')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const { runHookValidate } = await import('./commands/hook.js');
|
|
52
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
53
|
+
const stdin = await readStdin();
|
|
54
|
+
const { code, stderr } = await runHookValidate(resolveRepoRoot(process.cwd(), program.opts().repo), stdin);
|
|
55
|
+
if (stderr)
|
|
56
|
+
process.stderr.write(stderr + '\n');
|
|
57
|
+
process.exit(code);
|
|
58
|
+
});
|
|
59
|
+
const packCmd = program.command('pack').description('manage the installed discipline pack');
|
|
60
|
+
packCmd
|
|
61
|
+
.command('sync')
|
|
62
|
+
.description('re-install the bundled skills + directive at the current pack version (clears the stale nudge)')
|
|
63
|
+
.action(async () => {
|
|
64
|
+
const { runPackSync } = await import('./commands/pack.js');
|
|
65
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
66
|
+
const { location } = runPackSync(resolveRepoRoot(process.cwd(), program.opts().repo));
|
|
67
|
+
process.stdout.write(`pack synced (${location})\n`);
|
|
68
|
+
});
|
|
69
|
+
// Every other verb is registered with the same shape; actions are filled in by later tasks.
|
|
70
|
+
// Each registration below mirrors the verb's flags exactly so the surface is frozen now.
|
|
71
|
+
program
|
|
72
|
+
.command('init')
|
|
73
|
+
.argument('[projectId]', 'project slug (default: repo dir name)')
|
|
74
|
+
.option('--name <name>', 'display name (default: projectId)')
|
|
75
|
+
.option('--server <url>', 'Threadit server endpoint')
|
|
76
|
+
.option('--token <tok>', 'ingest token (prefer THREADIT_TOKEN env)')
|
|
77
|
+
.option('--project <id>', 'project id (overrides the positional)')
|
|
78
|
+
.option('--skills <loc>', 'skills install location: global|repo (default: prompt, then global)')
|
|
79
|
+
.option('--yes', 'non-interactive: accept defaults (skills=global)')
|
|
80
|
+
.description('install the Threadit pack into this repo (config + hooks + skills)')
|
|
81
|
+
.action(async (projectId, opts) => {
|
|
82
|
+
const { runInit } = await import('./commands/init.js');
|
|
83
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
84
|
+
const root = resolveRepoRoot(process.cwd(), program.opts().repo);
|
|
85
|
+
const id = opts.project ?? projectId ?? root.split('/').filter(Boolean).pop();
|
|
86
|
+
const resolvedToken = opts.token ?? process.env.THREADIT_TOKEN;
|
|
87
|
+
const { skillsLocation, warnings } = runInit(root, id, {
|
|
88
|
+
...(opts.name ? { name: opts.name } : {}),
|
|
89
|
+
...(opts.server ? { server: opts.server } : {}),
|
|
90
|
+
...(resolvedToken ? { token: resolvedToken } : {}),
|
|
91
|
+
...(opts.skills === 'global' || opts.skills === 'repo' ? { skills: opts.skills } : {}),
|
|
92
|
+
...(opts.yes ? { yes: true } : {}),
|
|
93
|
+
});
|
|
94
|
+
for (const w of warnings)
|
|
95
|
+
process.stderr.write(w + '\n');
|
|
96
|
+
process.stdout.write(`Initialized threadit project "${id}" in ${root} (skills: ${skillsLocation})\n`);
|
|
97
|
+
});
|
|
98
|
+
program
|
|
99
|
+
.command('session')
|
|
100
|
+
.option('--bump', 'increment the session counter (run once per session, e.g. from a SessionStart hook)')
|
|
101
|
+
.description('print the current session number, or bump it')
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
const { readSession, bumpSession } = await import('./session.js');
|
|
104
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
105
|
+
const repoRoot = resolveRepoRoot(process.cwd(), program.opts().repo);
|
|
106
|
+
const n = opts.bump ? bumpSession(repoRoot) : readSession(repoRoot);
|
|
107
|
+
process.stdout.write(`s${n}\n`);
|
|
108
|
+
});
|
|
109
|
+
program
|
|
110
|
+
.command('wrapup')
|
|
111
|
+
.description('stamp the wrap-up watermark (wrapup.session = current session) — called by the wrap-up skill at clean session end')
|
|
112
|
+
.action(async () => {
|
|
113
|
+
const { runWrapup } = await import('./commands/wrapup.js');
|
|
114
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
115
|
+
const n = runWrapup(resolveRepoRoot(process.cwd(), program.opts().repo));
|
|
116
|
+
process.stdout.write(`wrapped through s${n}\n`);
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command('sync')
|
|
120
|
+
.option('--draft', 'push the uncommitted working file as a draft overlay')
|
|
121
|
+
.option('--session <key>', 'session key for the draft overlay')
|
|
122
|
+
.description('push committed history (or the draft) to the server')
|
|
123
|
+
.action(async (opts) => {
|
|
124
|
+
const { runSync } = await import('./commands/sync.js');
|
|
125
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
126
|
+
const syncOpts = {
|
|
127
|
+
...(opts.draft ? { draft: true } : {}),
|
|
128
|
+
...(opts.session !== undefined ? { sessionKey: opts.session } : {}),
|
|
129
|
+
};
|
|
130
|
+
await runSync(resolveRepoRoot(process.cwd(), program.opts().repo), syncOpts);
|
|
131
|
+
process.stdout.write(opts.draft ? 'draft synced\n' : 'synced\n');
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command('status')
|
|
135
|
+
.description('orient: working slice + findings + sha self-heal')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
const { runStatus, formatReadingList } = await import('./commands/status.js');
|
|
138
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
139
|
+
const report = runStatus(resolveRepoRoot(process.cwd(), program.opts().repo));
|
|
140
|
+
if (program.opts().json) {
|
|
141
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`Working slice (${report.workingSlice.length}): ${report.workingSlice.join(', ') || '(empty)'}`);
|
|
146
|
+
if (report.findings.length) {
|
|
147
|
+
lines.push('Findings:');
|
|
148
|
+
for (const f of report.findings)
|
|
149
|
+
lines.push(` [${f.severityClass}] ${f.kind} @ ${f.target}${f.muted ? ' (muted)' : ''}: ${f.message}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
lines.push('Findings: none');
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push(...formatReadingList(report.readingList));
|
|
156
|
+
if (report.unsynced)
|
|
157
|
+
lines.push(`⚠ HEAD ${report.head ?? '(none)'} differs from last-synced ${report.lastSha ?? '(none)'} — run threadit sync`);
|
|
158
|
+
if (report.priorSessionUnwrapped) {
|
|
159
|
+
const from = (report.wrappedThrough ?? 0) + 1;
|
|
160
|
+
const to = report.currentSession - 1;
|
|
161
|
+
const label = from === to ? `session ${from}` : `sessions ${from}…${to}`;
|
|
162
|
+
lines.push(`⚠ ${label} ended without wrap-up — re-scan for uncaptured loops`);
|
|
163
|
+
}
|
|
164
|
+
lines.push('(working slice omits the "recently-closed" window — deferred to Phase 3 server data)');
|
|
165
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
166
|
+
});
|
|
167
|
+
program
|
|
168
|
+
.command('show')
|
|
169
|
+
.argument('<id>', 'node id')
|
|
170
|
+
.description('print one node in full')
|
|
171
|
+
.action(async (id) => {
|
|
172
|
+
const { runShow } = await import('./commands/show.js');
|
|
173
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
174
|
+
const node = runShow(resolveRepoRoot(process.cwd(), program.opts().repo), id);
|
|
175
|
+
process.stdout.write(JSON.stringify(node, null, 2) + '\n');
|
|
176
|
+
});
|
|
177
|
+
program
|
|
178
|
+
.command('ls')
|
|
179
|
+
.description('list nodes')
|
|
180
|
+
.action(async () => {
|
|
181
|
+
const { runLs } = await import('./commands/ls.js');
|
|
182
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
183
|
+
const rows = runLs(resolveRepoRoot(process.cwd(), program.opts().repo));
|
|
184
|
+
if (program.opts().json) {
|
|
185
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const r of rows)
|
|
189
|
+
process.stdout.write(`${' '.repeat(r.depth)}${r.id} [${r.status}] ${r.title}\n`);
|
|
190
|
+
});
|
|
191
|
+
program
|
|
192
|
+
.command('add')
|
|
193
|
+
.argument('<title>', 'node title')
|
|
194
|
+
.option('--id <id>').option('--parent <id>').option('--order <n>', 'order', parseIntOpt)
|
|
195
|
+
.option('--status <s>', 'status', 'planned').option('--flow <f>')
|
|
196
|
+
.description('add a node')
|
|
197
|
+
.action(async (title, opts) => {
|
|
198
|
+
const { runAdd } = await import('./commands/add.js');
|
|
199
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
200
|
+
runAdd(resolveRepoRoot(process.cwd(), program.opts().repo), title, opts);
|
|
201
|
+
process.stdout.write('added\n');
|
|
202
|
+
});
|
|
203
|
+
program
|
|
204
|
+
.command('move')
|
|
205
|
+
.argument('<id>', 'node id')
|
|
206
|
+
.option('--parent <id>').option('--order <n>', 'order', parseIntOpt)
|
|
207
|
+
.description('move a node (parent/order, with sibling rebalance)')
|
|
208
|
+
.action(async (id, opts) => {
|
|
209
|
+
const { runMove } = await import('./commands/move.js');
|
|
210
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
211
|
+
runMove(resolveRepoRoot(process.cwd(), program.opts().repo), id, opts);
|
|
212
|
+
process.stdout.write('moved\n');
|
|
213
|
+
});
|
|
214
|
+
program
|
|
215
|
+
.command('update <id>')
|
|
216
|
+
.option('--status <s>').option('--title <t>').option('--summary <s>')
|
|
217
|
+
.option('--summary-at <sha>').option('--detail <d>').option('--flow <f>')
|
|
218
|
+
.option('--milestone <bool>', 'milestone', (v) => v === 'true')
|
|
219
|
+
.description('update node fields')
|
|
220
|
+
.action(async (id, opts) => {
|
|
221
|
+
const { runUpdate } = await import('./commands/update.js');
|
|
222
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
223
|
+
runUpdate(resolveRepoRoot(process.cwd(), program.opts().repo), id, opts);
|
|
224
|
+
process.stdout.write('updated\n');
|
|
225
|
+
});
|
|
226
|
+
const docCmd = program.command('doc').description("manage a node's governing doc-links");
|
|
227
|
+
docCmd
|
|
228
|
+
.command('add <id>')
|
|
229
|
+
.requiredOption('--role <role>', 'handoff|memory|spec|plan|playbook')
|
|
230
|
+
.requiredOption('--ref <ref>', 'path, URL, or memory slug')
|
|
231
|
+
.description('add a doc-link to a node')
|
|
232
|
+
.action(async (id, opts) => {
|
|
233
|
+
const { runDocAdd } = await import('./commands/doc.js');
|
|
234
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
235
|
+
const { warning } = runDocAdd(resolveRepoRoot(process.cwd(), program.opts().repo), id, opts.role, opts.ref);
|
|
236
|
+
if (warning)
|
|
237
|
+
process.stderr.write(warning + '\n');
|
|
238
|
+
process.stdout.write('added\n');
|
|
239
|
+
});
|
|
240
|
+
docCmd
|
|
241
|
+
.command('rm <id>')
|
|
242
|
+
.requiredOption('--ref <ref>', 'the ref to remove')
|
|
243
|
+
.option('--role <role>', 'narrow removal to one role')
|
|
244
|
+
.description('remove a doc-link from a node')
|
|
245
|
+
.action(async (id, opts) => {
|
|
246
|
+
const { runDocRm } = await import('./commands/doc.js');
|
|
247
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
248
|
+
runDocRm(resolveRepoRoot(process.cwd(), program.opts().repo), id, opts.ref, opts.role);
|
|
249
|
+
process.stdout.write('removed\n');
|
|
250
|
+
});
|
|
251
|
+
docCmd
|
|
252
|
+
.command('ls <id>')
|
|
253
|
+
.option('--json', 'output as JSON')
|
|
254
|
+
.description("list a node's doc-links")
|
|
255
|
+
.action(async (id, opts) => {
|
|
256
|
+
const { runDocList } = await import('./commands/doc.js');
|
|
257
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
258
|
+
const docs = runDocList(resolveRepoRoot(process.cwd(), program.opts().repo), id);
|
|
259
|
+
// `--json` may land on the root program (Commander attributes a trailing flag after the
|
|
260
|
+
// positional to the parent); the local --option declaration above just prevents an
|
|
261
|
+
// "unknown option" error, while program.opts().json carries the value.
|
|
262
|
+
if (opts.json || program.opts().json) {
|
|
263
|
+
process.stdout.write(JSON.stringify(docs, null, 2) + '\n');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
for (const d of docs)
|
|
267
|
+
process.stdout.write(`${d.role}\t${d.ref}\n`);
|
|
268
|
+
});
|
|
269
|
+
program
|
|
270
|
+
.command('ship')
|
|
271
|
+
.argument('<id>', 'node id')
|
|
272
|
+
.description('flip to shipped (commit-binding courtesy check)')
|
|
273
|
+
.action(async (id) => {
|
|
274
|
+
const { runShip } = await import('./commands/ship.js');
|
|
275
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
276
|
+
const { warning } = runShip(resolveRepoRoot(process.cwd(), program.opts().repo), id);
|
|
277
|
+
if (warning)
|
|
278
|
+
process.stderr.write(warning + '\n');
|
|
279
|
+
process.stdout.write(`shipped ${id}\n`);
|
|
280
|
+
});
|
|
281
|
+
program
|
|
282
|
+
.command('park')
|
|
283
|
+
.argument('<id>', 'node id')
|
|
284
|
+
.option('--until <json>', 'until predicate as JSON')
|
|
285
|
+
.description('park a node')
|
|
286
|
+
.action(async (id, opts) => {
|
|
287
|
+
const { runPark } = await import('./commands/park.js');
|
|
288
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
289
|
+
const until = opts.until !== undefined ? JSON.parse(opts.until) : undefined;
|
|
290
|
+
runPark(resolveRepoRoot(process.cwd(), program.opts().repo), id, until);
|
|
291
|
+
process.stdout.write('parked\n');
|
|
292
|
+
});
|
|
293
|
+
program
|
|
294
|
+
.command('supersede')
|
|
295
|
+
.argument('<id>', 'node id')
|
|
296
|
+
.requiredOption('--by <id>', 'the successor node')
|
|
297
|
+
.description('supersede a node')
|
|
298
|
+
.action(async (id, opts) => {
|
|
299
|
+
const { runSupersede } = await import('./commands/supersede.js');
|
|
300
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
301
|
+
runSupersede(resolveRepoRoot(process.cwd(), program.opts().repo), id, opts.by);
|
|
302
|
+
process.stdout.write('superseded\n');
|
|
303
|
+
});
|
|
304
|
+
program
|
|
305
|
+
.command('fold')
|
|
306
|
+
.argument('<id>', 'node id')
|
|
307
|
+
.requiredOption('--into <id>', 'the absorbing node')
|
|
308
|
+
.description('fold a node into another')
|
|
309
|
+
.action(async (id, opts) => {
|
|
310
|
+
const { runFold } = await import('./commands/fold.js');
|
|
311
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
312
|
+
runFold(resolveRepoRoot(process.cwd(), program.opts().repo), id, opts.into);
|
|
313
|
+
process.stdout.write('folded\n');
|
|
314
|
+
});
|
|
315
|
+
program
|
|
316
|
+
.command('capture')
|
|
317
|
+
.argument('<text...>', 'the capture text')
|
|
318
|
+
.option('--at <label>', 'time label (session number or ISO date)')
|
|
319
|
+
.option('--tags <csv>', 'comma-separated tags')
|
|
320
|
+
.option('--risk-node <id>', 'glow anchor for a live-risk item')
|
|
321
|
+
.description('capture an inbox item')
|
|
322
|
+
.action(async (textParts, opts) => {
|
|
323
|
+
const { runCapture } = await import('./commands/capture.js');
|
|
324
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
325
|
+
const text = textParts.join(' ');
|
|
326
|
+
const at = opts.at === undefined ? undefined : (/^\d+$/.test(opts.at) ? Number(opts.at) : opts.at);
|
|
327
|
+
const captureOpts = {
|
|
328
|
+
...(at !== undefined ? { at } : {}),
|
|
329
|
+
...(opts.tags ? { tags: opts.tags.split(',').map((s) => s.trim()).filter(Boolean) } : {}),
|
|
330
|
+
...(opts.riskNode !== undefined ? { riskNode: opts.riskNode } : {}),
|
|
331
|
+
};
|
|
332
|
+
const { id, recurrence } = runCapture(resolveRepoRoot(process.cwd(), program.opts().repo), text, captureOpts);
|
|
333
|
+
process.stdout.write(`${recurrence ? 'recurrence on' : 'captured'} ${id}\n`);
|
|
334
|
+
});
|
|
335
|
+
program
|
|
336
|
+
.command('promote')
|
|
337
|
+
.argument('<itemId>', 'inbox item id')
|
|
338
|
+
.requiredOption('--under <parentId>', 'parent node')
|
|
339
|
+
.option('--id <id>').option('--status <s>', 'status', 'planned').option('--title <t>')
|
|
340
|
+
.description('promote an inbox item to a node')
|
|
341
|
+
.action(async (itemId, opts) => {
|
|
342
|
+
const { runPromote } = await import('./commands/promote.js');
|
|
343
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
344
|
+
const nodeId = runPromote(resolveRepoRoot(process.cwd(), program.opts().repo), itemId, opts);
|
|
345
|
+
process.stdout.write(`promoted ${itemId} -> ${nodeId}\n`);
|
|
346
|
+
});
|
|
347
|
+
program
|
|
348
|
+
.command('merge')
|
|
349
|
+
.argument('<itemId>', 'inbox item id')
|
|
350
|
+
.requiredOption('--into <itemId>', 'the sibling item')
|
|
351
|
+
.description('merge an inbox item into a recurring sibling')
|
|
352
|
+
.action(async (itemId, opts) => {
|
|
353
|
+
const { runMerge } = await import('./commands/merge.js');
|
|
354
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
355
|
+
runMerge(resolveRepoRoot(process.cwd(), program.opts().repo), itemId, opts.into);
|
|
356
|
+
process.stdout.write(`merged ${itemId} -> ${opts.into}\n`);
|
|
357
|
+
});
|
|
358
|
+
program
|
|
359
|
+
.command('drop')
|
|
360
|
+
.argument('<itemId>', 'inbox item id')
|
|
361
|
+
.description('drop an inbox item')
|
|
362
|
+
.action(async (itemId) => {
|
|
363
|
+
const { runDrop } = await import('./commands/drop.js');
|
|
364
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
365
|
+
runDrop(resolveRepoRoot(process.cwd(), program.opts().repo), itemId);
|
|
366
|
+
process.stdout.write(`dropped ${itemId}\n`);
|
|
367
|
+
});
|
|
368
|
+
program
|
|
369
|
+
.command('accept <nodeId>')
|
|
370
|
+
.requiredOption('--kind <kind>', 'finding kind to accept')
|
|
371
|
+
.requiredOption('--target <id>', 'finding target (node or edge id)')
|
|
372
|
+
.requiredOption('--until <json>', 'until-predicate as JSON')
|
|
373
|
+
.requiredOption('--text <prose>', 'rationale (authored by the caller)')
|
|
374
|
+
.option('--id <id>', 'decision id (default: accept-<kind>-<target>)')
|
|
375
|
+
.option('--at <label>', 'time label')
|
|
376
|
+
.description('mute a review-class finding via a decisions: record')
|
|
377
|
+
.action(async (nodeId, opts) => {
|
|
378
|
+
const { runAccept } = await import('./commands/accept.js');
|
|
379
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
380
|
+
const root = resolveRepoRoot(process.cwd(), program.opts().repo);
|
|
381
|
+
runAccept(root, nodeId, {
|
|
382
|
+
kind: opts.kind, target: opts.target, until: JSON.parse(opts.until), text: opts.text,
|
|
383
|
+
...(opts.id ? { id: opts.id } : {}), ...(opts.at ? { at: opts.at } : {}),
|
|
384
|
+
});
|
|
385
|
+
process.stdout.write(`accepted ${opts.kind} on ${opts.target}\n`);
|
|
386
|
+
});
|
|
387
|
+
program
|
|
388
|
+
.command('snooze')
|
|
389
|
+
.argument('<itemId>', 'inbox item id')
|
|
390
|
+
.requiredOption('--until <json>', 'until predicate as JSON')
|
|
391
|
+
.description('snooze an inbox item (requires a wake trigger)')
|
|
392
|
+
.action(async (itemId, opts) => {
|
|
393
|
+
const { runSnooze } = await import('./commands/snooze.js');
|
|
394
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
395
|
+
runSnooze(resolveRepoRoot(process.cwd(), program.opts().repo), itemId, JSON.parse(opts.until));
|
|
396
|
+
process.stdout.write(`snoozed ${itemId}\n`);
|
|
397
|
+
});
|
|
398
|
+
program
|
|
399
|
+
.command('reconcile')
|
|
400
|
+
.option('--apply <json>', 'disposition map as JSON')
|
|
401
|
+
.description('wrap-up: triage the inbox + reconcile claimed-vs-committed status + surface un-captured loops')
|
|
402
|
+
.action(async (opts) => {
|
|
403
|
+
const { runReconcile } = await import('./commands/reconcile.js');
|
|
404
|
+
const { resolveRepoRoot } = await import('./paths.js');
|
|
405
|
+
const root = resolveRepoRoot(process.cwd(), program.opts().repo);
|
|
406
|
+
const applyArg = opts.apply !== undefined ? { apply: JSON.parse(opts.apply) } : {};
|
|
407
|
+
const { proposals } = runReconcile(root, applyArg);
|
|
408
|
+
if (opts.apply === undefined) {
|
|
409
|
+
if (program.opts().json) {
|
|
410
|
+
process.stdout.write(JSON.stringify(proposals, null, 2) + '\n');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const { formatProposals } = await import('./reconcileFormat.js');
|
|
414
|
+
process.stdout.write(formatProposals(proposals));
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
process.stdout.write('reconciled\n');
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
program
|
|
421
|
+
.command('serve')
|
|
422
|
+
.option('--port <n>', 'port', parseIntOpt)
|
|
423
|
+
.option('--db <path>', 'SQLite db path (default: ./.threadit/server.sqlite)')
|
|
424
|
+
.description('boot the Threadit server on localhost (spawns the built SvelteKit server)')
|
|
425
|
+
.action(async (opts) => {
|
|
426
|
+
const { runServe } = await import('./commands/serve.js');
|
|
427
|
+
const serveOpts = {
|
|
428
|
+
...(opts.port !== undefined ? { port: opts.port } : {}),
|
|
429
|
+
...(opts.db !== undefined ? { dbPath: opts.db } : {}),
|
|
430
|
+
};
|
|
431
|
+
runServe(serveOpts);
|
|
432
|
+
process.stdout.write(`Threadit server spawning on port ${opts.port ?? 4317}\n`);
|
|
433
|
+
});
|
|
434
|
+
// Post-action best-effort pushes — never fail the user's command.
|
|
435
|
+
// Draft: stream threadit.yml as a session-keyed overlay if it diverged from HEAD.
|
|
436
|
+
// Inbox: re-push the inbox after any inbox-mutating verb.
|
|
437
|
+
program.hook('postAction', async (_thisCommand, actionCommand) => {
|
|
438
|
+
const name = actionCommand.name();
|
|
439
|
+
try {
|
|
440
|
+
const repoRoot = resolveRepoRoot(process.cwd(), program.opts().repo);
|
|
441
|
+
if (shouldPushAfter(name))
|
|
442
|
+
await maybePushDraft(repoRoot, defaultDraftPushDeps(repoRoot));
|
|
443
|
+
if (shouldPushInboxAfter(name))
|
|
444
|
+
await maybePushInbox(repoRoot, defaultInboxPushDeps(repoRoot));
|
|
445
|
+
}
|
|
446
|
+
catch { /* best-effort — never fail the user's command */ }
|
|
447
|
+
});
|
|
448
|
+
return program;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* True iff this module is the process entrypoint. Robust to the npm-bin symlink case:
|
|
452
|
+
* when installed, the binary is a symlink (e.g. `.../bin/threadit`) whose name is NOT `bin.js`,
|
|
453
|
+
* so the old `argv[1].endsWith('bin.js')` heuristic silently no-op'd the entire CLI. We resolve
|
|
454
|
+
* argv[1]'s realpath (following the symlink) and compare it to this module's own path.
|
|
455
|
+
*/
|
|
456
|
+
export function isBinInvocation(argv1, moduleHref) {
|
|
457
|
+
if (!argv1)
|
|
458
|
+
return false;
|
|
459
|
+
try {
|
|
460
|
+
return realpathSync(argv1) === fileURLToPath(moduleHref);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Invocation guard: only parse argv when run as the bin, not when imported by tests.
|
|
467
|
+
if (isBinInvocation(process.argv[1], import.meta.url)) {
|
|
468
|
+
buildProgram().parseAsync(process.argv).catch((err) => {
|
|
469
|
+
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
470
|
+
process.exit(2);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ParsedThreaditFile, FindingKind, UntilPredicate } from '@threadit/core';
|
|
2
|
+
export interface AcceptOpts {
|
|
3
|
+
kind: FindingKind;
|
|
4
|
+
target: string;
|
|
5
|
+
until: UntilPredicate;
|
|
6
|
+
text: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
at?: string | number;
|
|
9
|
+
}
|
|
10
|
+
/** Pure core: mutate a parsed threadit file in-place. */
|
|
11
|
+
export declare function applyAccept(file: ParsedThreaditFile, nodeId: string, opts: AcceptOpts): void;
|
|
12
|
+
export declare function runAccept(repoRoot: string, nodeId: string, opts: AcceptOpts): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolveThreaditPath } from '../paths.js';
|
|
2
|
+
import { mutate, findNode } from '../mutate.js';
|
|
3
|
+
/** Pure core: mutate a parsed threadit file in-place. */
|
|
4
|
+
export function applyAccept(file, nodeId, opts) {
|
|
5
|
+
const id = opts.id ?? `accept-${opts.kind}-${opts.target}`;
|
|
6
|
+
const node = findNode(file, nodeId);
|
|
7
|
+
const record = {
|
|
8
|
+
id, status: 'executed', text: opts.text,
|
|
9
|
+
accepts: { kind: opts.kind, target: opts.target }, until: opts.until,
|
|
10
|
+
...(opts.at !== undefined ? { at: opts.at } : {}),
|
|
11
|
+
};
|
|
12
|
+
node.decisions ??= [];
|
|
13
|
+
const existing = node.decisions.findIndex((d) => d.id === id);
|
|
14
|
+
if (existing >= 0)
|
|
15
|
+
node.decisions[existing] = record;
|
|
16
|
+
else
|
|
17
|
+
node.decisions.push(record);
|
|
18
|
+
}
|
|
19
|
+
export function runAccept(repoRoot, nodeId, opts) {
|
|
20
|
+
mutate(resolveThreaditPath(repoRoot), (file) => applyAccept(file, nodeId, opts));
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ParsedThreaditFile, Status, Flow } from '@threadit/core';
|
|
2
|
+
export interface AddOpts {
|
|
3
|
+
id?: string;
|
|
4
|
+
parent?: string;
|
|
5
|
+
order?: number;
|
|
6
|
+
status?: Status;
|
|
7
|
+
flow?: Flow;
|
|
8
|
+
}
|
|
9
|
+
/** Pure core: mutate a parsed threadit file in-place; returns the created node id. */
|
|
10
|
+
export declare function applyAddNode(file: ParsedThreaditFile, title: string, opts: AddOpts): string;
|
|
11
|
+
export declare function runAdd(repoRoot: string, title: string, opts: AddOpts): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { resolveThreaditPath } from '../paths.js';
|
|
2
|
+
import { mutate } from '../mutate.js';
|
|
3
|
+
import { slugify, uniqueNodeId } from '../ids.js';
|
|
4
|
+
/** Pure core: mutate a parsed threadit file in-place; returns the created node id. */
|
|
5
|
+
export function applyAddNode(file, title, opts) {
|
|
6
|
+
const id = uniqueNodeId(file, opts.id ?? slugify(title));
|
|
7
|
+
const siblings = file.nodes.filter((n) => (n.parent ?? null) === (opts.parent ?? null));
|
|
8
|
+
const order = opts.order ?? (siblings.length ? Math.max(...siblings.map((s) => s.order)) + 10 : 10);
|
|
9
|
+
const node = { id, order, title, status: opts.status ?? 'planned' };
|
|
10
|
+
if (opts.parent !== undefined)
|
|
11
|
+
node.parent = opts.parent;
|
|
12
|
+
if (opts.flow !== undefined)
|
|
13
|
+
node.flow = opts.flow;
|
|
14
|
+
file.nodes.push(node);
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
export function runAdd(repoRoot, title, opts) {
|
|
18
|
+
mutate(resolveThreaditPath(repoRoot), (file) => { applyAddNode(file, title, opts); });
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ParsedInboxFile, TimeLabel } from '@threadit/core';
|
|
2
|
+
export interface CaptureOpts {
|
|
3
|
+
at?: TimeLabel;
|
|
4
|
+
tags?: string[];
|
|
5
|
+
riskNode?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Pure core: capture an inbox item into a parsed inbox object. If it fuzzy-matches an existing
|
|
9
|
+
* OPEN item, record a recurrence (seen_count++ / append seen_at) instead of duplicating.
|
|
10
|
+
* The `at` default (new Date().toISOString()) lives here so Task 9 can call applyCapture
|
|
11
|
+
* with an empty opts and still get a timestamp. Returns the affected item id + recurrence flag.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyCapture(inbox: ParsedInboxFile, text: string, opts: CaptureOpts): {
|
|
14
|
+
id: string;
|
|
15
|
+
recurrence: boolean;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Capture an inbox item. If it fuzzy-matches an existing OPEN item, record a recurrence
|
|
19
|
+
* (seen_count++ / append seen_at) instead of duplicating. Returns the affected item id.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runCapture(repoRoot: string, text: string, opts: CaptureOpts): {
|
|
22
|
+
id: string;
|
|
23
|
+
recurrence: boolean;
|
|
24
|
+
};
|