omni-pi 0.10.1 → 0.12.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/CHANGELOG.md +49 -0
- package/README.md +2 -25
- package/bin/omni.d.ts +22 -0
- package/bin/omni.js +42 -6
- package/extensions/omni-core/index.ts +0 -4
- package/package.json +11 -13
- package/src/atomic.ts +94 -0
- package/src/config.ts +3 -2
- package/src/header.ts +6 -3
- package/src/planning.ts +15 -5
- package/src/plans.ts +7 -8
- package/src/prompt-template-sync.ts +4 -8
- package/src/providers.ts +11 -707
- package/src/repo-map-contracts.ts +7 -3
- package/src/repo-map-index.ts +70 -11
- package/src/repo-map-runtime.ts +16 -7
- package/src/repo-map-store.ts +3 -3
- package/src/skills.ts +80 -20
- package/src/standards.ts +15 -8
- package/src/sync.ts +38 -14
- package/src/tasks.ts +11 -3
- package/src/theme.ts +4 -3
- package/src/updater.ts +71 -13
- package/src/work.ts +5 -9
- package/src/workflow.ts +43 -16
- package/PROVIDERS.md +0 -80
- package/extensions/omni-providers/index.ts +0 -16
- package/src/anthropic-auth-guard.ts +0 -95
- package/src/model-command.ts +0 -205
- package/src/model-refresh-state.ts +0 -39
- package/src/model-setup.ts +0 -1149
- package/src/provider-auth-command.ts +0 -104
- package/src/provider-catalog.json +0 -20391
- package/src/searchable-select.ts +0 -198
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 - 2026-04-27
|
|
4
|
+
|
|
5
|
+
### Security and robustness
|
|
6
|
+
|
|
7
|
+
- Sanitized untrusted text before it reaches brain prompts and `DECISIONS.md`
|
|
8
|
+
- Sanitized generated `SKILL.md` files and fixed `Set` mutation in loop
|
|
9
|
+
- Added atomic writes for `.omni/` and `.pi/` state files
|
|
10
|
+
- Hardened version parsing in the self-updater
|
|
11
|
+
|
|
12
|
+
### Bug fixes
|
|
13
|
+
|
|
14
|
+
- Fixed `isRequestRelated` to actually use the overlap ratio for planning continuity
|
|
15
|
+
- Fixed repo-map indexing, fingerprints, and dirty-path tracking
|
|
16
|
+
- Fixed backtick code span tracking in the task table parser
|
|
17
|
+
- Fixed prerelease ordering so pre-releases sort below their matching release in the updater
|
|
18
|
+
- Used `os.homedir()` for reliable home directory resolution in the updater
|
|
19
|
+
- Cached package version at module load to avoid repeated filesystem reads in the header
|
|
20
|
+
|
|
21
|
+
### Dependencies
|
|
22
|
+
|
|
23
|
+
- Upgraded `@mariozechner/pi-coding-agent` to `0.70.2`
|
|
24
|
+
- Upgraded `@anthropic-ai/claude-agent-sdk` to `0.2.119`
|
|
25
|
+
- Upgraded `@juanibiapina/pi-powerbar` to `0.9.1`
|
|
26
|
+
- Upgraded `pi-interview` to `0.8.6`
|
|
27
|
+
- Upgraded `pi-prompt-template-model` to `0.9.1`
|
|
28
|
+
- Upgraded `glimpseui` to `0.8.0`
|
|
29
|
+
- Upgraded `@biomejs/biome` to `2.4.13`
|
|
30
|
+
- Upgraded `typescript` to `6.0.3`
|
|
31
|
+
- Upgraded `vitest` to `4.1.5`
|
|
32
|
+
- Upgraded `@types/node` to `25.6.0`
|
|
33
|
+
|
|
34
|
+
### Housekeeping
|
|
35
|
+
|
|
36
|
+
- Hoisted control-char regexes and applied Biome formatting
|
|
37
|
+
- Included tests in `tsc` check and added launcher type declarations
|
|
38
|
+
- Added `tsbuildinfo` and coverage artifacts to `.gitignore`
|
|
39
|
+
- Enforced commit-after-every-task rule in `AGENTS.md`
|
|
40
|
+
|
|
41
|
+
## 0.11.0 - 2026-04-24
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- Removed `/model-setup`, `/manage-providers`, the `omni-providers` extension, and the bundled provider catalog. Pi now handles provider and model management natively.
|
|
46
|
+
- Removed `PROVIDERS.md` and the provider docs spec.
|
|
47
|
+
|
|
3
48
|
## 0.10.1 - 2026-04-24
|
|
4
49
|
|
|
5
50
|
### Release follow-up
|
|
@@ -9,6 +54,10 @@
|
|
|
9
54
|
|
|
10
55
|
## 0.10.0 - 2026-04-24
|
|
11
56
|
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- Removed `/model-setup`, `/manage-providers`, and the `omni-providers` extension. Pi now handles provider and model management natively.
|
|
60
|
+
|
|
12
61
|
### Runtime and integrations
|
|
13
62
|
|
|
14
63
|
- upgraded `@mariozechner/pi-coding-agent` to `0.70.0`
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Requires Node.js 22 or newer.
|
|
|
15
15
|
- Keeps durable standards and project context in `.omni/`, even when Omni mode is off.
|
|
16
16
|
- Writes specs, tasks, and progress into `.omni/` once Omni mode is enabled.
|
|
17
17
|
- Adds a repo map that indexes supported source files, ranks them by structure plus recent activity, and injects a compact codebase-awareness block into Omni prompts.
|
|
18
|
-
- Bundles web search, guided interviews, themed UI, native micro-UI via Glimpse, native git diff review, prompt-template-powered workflow commands, a task viewer, a powerbar,
|
|
18
|
+
- Bundles web search, guided interviews, themed UI, native micro-UI via Glimpse, native git diff review, prompt-template-powered workflow commands, a task viewer, a powerbar, and automatic updates out of the box.
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
@@ -30,8 +30,6 @@ cd your-project
|
|
|
30
30
|
omni
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Custom provider setup, refresh behavior, and bundled provider behavior are documented in [PROVIDERS.md](PROVIDERS.md).
|
|
34
|
-
|
|
35
33
|
## Features
|
|
36
34
|
|
|
37
35
|
### Bundled Skills
|
|
@@ -66,7 +64,6 @@ Current deferred roadmap items remain intentional and visible in docs rather tha
|
|
|
66
64
|
| Extension | What it does |
|
|
67
65
|
|-----------|-------------|
|
|
68
66
|
| **omni-core** | Brain workflow, themed header, session init, system prompt injection |
|
|
69
|
-
| **omni-providers** | Model provider wiring |
|
|
70
67
|
| **omni-memory** | `.omni/` durable memory bootstrap |
|
|
71
68
|
| **glimpseui** | Native micro-UI windows and the optional floating companion widget |
|
|
72
69
|
| **pi-web-access** | Web search and fetch tools for the agent |
|
|
@@ -88,8 +85,6 @@ Omni-Pi now bundles [Glimpse](https://github.com/HazAT/glimpse) for native micro
|
|
|
88
85
|
|
|
89
86
|
| Command | Description |
|
|
90
87
|
|---------|-------------|
|
|
91
|
-
| `/model-setup` | Add, refresh, or remove custom provider/model entries |
|
|
92
|
-
| `/manage-providers` | Remove stored auth for bundled providers |
|
|
93
88
|
| `/omni-mode` | Toggle persistent Omni mode on or off for this project |
|
|
94
89
|
| `/companion` | Toggle the Glimpse floating companion widget |
|
|
95
90
|
| `/diff-review` | Open a native git diff review window and insert feedback into the editor |
|
|
@@ -108,24 +103,6 @@ Omni-Pi now bundles [Glimpse](https://github.com/HazAT/glimpse) for native micro
|
|
|
108
103
|
|
|
109
104
|
Omni-Pi checks for new versions on startup (cached, re-checks every 4 hours). When an update is available, it prompts to install and restart. Pi's own update notification is suppressed to avoid duplication.
|
|
110
105
|
|
|
111
|
-
## Provider Support
|
|
112
|
-
|
|
113
|
-
`/model-setup` is for custom providers and custom model entries only.
|
|
114
|
-
|
|
115
|
-
Use `/model-setup` when you want to configure:
|
|
116
|
-
|
|
117
|
-
- a custom provider id
|
|
118
|
-
- an API type and base URL
|
|
119
|
-
- an API key for that custom provider
|
|
120
|
-
- discovered models or manual model entries
|
|
121
|
-
- a manual refresh of already configured custom providers
|
|
122
|
-
|
|
123
|
-
Use `/manage-providers` to remove stored auth for bundled Pi providers.
|
|
124
|
-
|
|
125
|
-
Anthropic is intentionally API-key-only in Omni-Pi. Anthropic OAuth login is disabled.
|
|
126
|
-
|
|
127
|
-
See [PROVIDERS.md](PROVIDERS.md) for the current supported-provider list and auth-management split.
|
|
128
|
-
|
|
129
106
|
## Omni Mode
|
|
130
107
|
|
|
131
108
|
Omni-Pi keeps its current branding and shell at all times, but the specialized workflow is opt-in.
|
|
@@ -176,7 +153,7 @@ npm run chat # launch locally in dev mode
|
|
|
176
153
|
## CI/CD
|
|
177
154
|
|
|
178
155
|
- Pull requests and pushes to `main` run `npm run verify`.
|
|
179
|
-
- The docs are part of the test contract
|
|
156
|
+
- The docs are part of the test contract.
|
|
180
157
|
- Pushing a `v*` tag runs the release workflow, verifies the repo again, publishes to npm through GitHub Actions trusted publishing with provenance, and then creates the GitHub release.
|
|
181
158
|
- Trusted publishing still requires npm-side setup for this repository/workflow in the npm package settings.
|
|
182
159
|
|
package/bin/omni.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function getOmniPackageDir(): string;
|
|
2
|
+
export function resolvePiCliPath(): string;
|
|
3
|
+
export function buildOmniEnvironment(
|
|
4
|
+
baseEnv?: NodeJS.ProcessEnv,
|
|
5
|
+
): NodeJS.ProcessEnv;
|
|
6
|
+
export function ensureQuietStartupDefault(baseEnv?: NodeJS.ProcessEnv): void;
|
|
7
|
+
export function buildPiProcessSpec(
|
|
8
|
+
argv?: string[],
|
|
9
|
+
baseEnv?: NodeJS.ProcessEnv,
|
|
10
|
+
): {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
};
|
|
15
|
+
export function runOmni(
|
|
16
|
+
argv?: string[],
|
|
17
|
+
options?: { cwd?: string; env?: NodeJS.ProcessEnv },
|
|
18
|
+
): Promise<number>;
|
|
19
|
+
export function isOmniEntrypointInvocation(
|
|
20
|
+
argvPath?: string,
|
|
21
|
+
moduleUrl?: string,
|
|
22
|
+
): boolean;
|
package/bin/omni.js
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
statSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
5
15
|
import os from "node:os";
|
|
6
16
|
import path from "node:path";
|
|
7
17
|
import { fileURLToPath } from "node:url";
|
|
8
18
|
|
|
19
|
+
function writeFileAtomicSync(filePath, content) {
|
|
20
|
+
const tempPath = `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
21
|
+
let mode;
|
|
22
|
+
try {
|
|
23
|
+
mode = statSync(filePath).mode & 0o777;
|
|
24
|
+
} catch {
|
|
25
|
+
// New file: keep Node's default creation mode.
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(tempPath, content, "utf8");
|
|
29
|
+
if (mode !== undefined) {
|
|
30
|
+
chmodSync(tempPath, mode);
|
|
31
|
+
}
|
|
32
|
+
renameSync(tempPath, filePath);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
try {
|
|
35
|
+
unlinkSync(tempPath);
|
|
36
|
+
} catch {
|
|
37
|
+
// temp may not exist
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
export function getOmniPackageDir() {
|
|
10
44
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
45
|
}
|
|
@@ -22,8 +56,12 @@ export function resolvePiCliPath() {
|
|
|
22
56
|
}
|
|
23
57
|
|
|
24
58
|
export function buildOmniEnvironment(baseEnv = process.env) {
|
|
59
|
+
// Pi has its own update prompt; Omni-Pi runs its own (registerUpdater).
|
|
60
|
+
// Suppress Pi's check at the launcher boundary so the Omni updater is
|
|
61
|
+
// the only one that surfaces upgrade prompts.
|
|
25
62
|
return {
|
|
26
63
|
...baseEnv,
|
|
64
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
27
65
|
};
|
|
28
66
|
}
|
|
29
67
|
|
|
@@ -56,10 +94,9 @@ export function ensureQuietStartupDefault(baseEnv = process.env) {
|
|
|
56
94
|
typeof parsed === "object" &&
|
|
57
95
|
parsed.quietStartup === undefined
|
|
58
96
|
) {
|
|
59
|
-
|
|
97
|
+
writeFileAtomicSync(
|
|
60
98
|
settingsFile,
|
|
61
99
|
`${JSON.stringify({ ...parsed, quietStartup: true }, null, 2)}\n`,
|
|
62
|
-
"utf8",
|
|
63
100
|
);
|
|
64
101
|
}
|
|
65
102
|
} catch (error) {
|
|
@@ -73,10 +110,9 @@ export function ensureQuietStartupDefault(baseEnv = process.env) {
|
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
mkdirSync(agentDir, { recursive: true });
|
|
76
|
-
|
|
113
|
+
writeFileAtomicSync(
|
|
77
114
|
settingsFile,
|
|
78
115
|
`${JSON.stringify({ quietStartup: true }, null, 2)}\n`,
|
|
79
|
-
"utf8",
|
|
80
116
|
);
|
|
81
117
|
}
|
|
82
118
|
}
|
|
@@ -96,7 +132,7 @@ export async function runOmni(argv = process.argv.slice(2), options = {}) {
|
|
|
96
132
|
ensureQuietStartupDefault(options.env);
|
|
97
133
|
const spec = buildPiProcessSpec(argv, options.env);
|
|
98
134
|
|
|
99
|
-
await new Promise((resolve, reject) => {
|
|
135
|
+
return await new Promise((resolve, reject) => {
|
|
100
136
|
const child = spawn(spec.command, spec.args, {
|
|
101
137
|
cwd: options.cwd ?? process.cwd(),
|
|
102
138
|
env: spec.env,
|
|
@@ -9,13 +9,11 @@ import {
|
|
|
9
9
|
} from "../../src/brain.js";
|
|
10
10
|
import { createOmniCommands } from "../../src/commands.js";
|
|
11
11
|
import { renderHeader } from "../../src/header.js";
|
|
12
|
-
import { registerModelCommand } from "../../src/model-command.js";
|
|
13
12
|
import {
|
|
14
13
|
registerOmniMessageRenderer,
|
|
15
14
|
registerPiCommands,
|
|
16
15
|
} from "../../src/pi.js";
|
|
17
16
|
import { ensureBundledPromptTemplates } from "../../src/prompt-template-sync.js";
|
|
18
|
-
import { registerProviderAuthCommand } from "../../src/provider-auth-command.js";
|
|
19
17
|
import {
|
|
20
18
|
buildRepoMapPromptSuffix,
|
|
21
19
|
registerRepoMapTracking,
|
|
@@ -42,8 +40,6 @@ import { buildOnboardingInterviewKickoff } from "../../src/workflow.js";
|
|
|
42
40
|
export default function omniCoreExtension(api: ExtensionAPI): void {
|
|
43
41
|
registerOmniMessageRenderer(api);
|
|
44
42
|
registerPiCommands(api, createOmniCommands());
|
|
45
|
-
registerModelCommand(api);
|
|
46
|
-
registerProviderAuthCommand(api);
|
|
47
43
|
registerThemeCommand(api);
|
|
48
44
|
registerTodoShortcut(api);
|
|
49
45
|
registerUpdater(api);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omni-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Single-agent Pi package that interviews the user, documents the spec, and implements work in bounded slices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"templates",
|
|
39
39
|
"vendor/pi-diff-review",
|
|
40
40
|
"README.md",
|
|
41
|
-
"PROVIDERS.md",
|
|
42
41
|
"CREDITS.md"
|
|
43
42
|
],
|
|
44
43
|
"scripts": {
|
|
@@ -52,14 +51,13 @@
|
|
|
52
51
|
"prepublishOnly": "npm run verify"
|
|
53
52
|
},
|
|
54
53
|
"devDependencies": {
|
|
55
|
-
"@biomejs/biome": "2.4.
|
|
56
|
-
"@types/node": "^
|
|
57
|
-
"typescript": "^
|
|
58
|
-
"vitest": "^
|
|
54
|
+
"@biomejs/biome": "2.4.13",
|
|
55
|
+
"@types/node": "^25.6.0",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vitest": "^4.1.5"
|
|
59
58
|
},
|
|
60
59
|
"pi": {
|
|
61
60
|
"extensions": [
|
|
62
|
-
"./extensions/omni-providers/index.ts",
|
|
63
61
|
"./extensions/omni-core/index.ts",
|
|
64
62
|
"./extensions/omni-memory/index.ts",
|
|
65
63
|
"./node_modules/glimpseui/pi-extension/index.ts",
|
|
@@ -81,14 +79,14 @@
|
|
|
81
79
|
]
|
|
82
80
|
},
|
|
83
81
|
"dependencies": {
|
|
84
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.
|
|
82
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
|
|
85
83
|
"@juanibiapina/pi-extension-settings": "^0.6.1",
|
|
86
|
-
"@juanibiapina/pi-powerbar": "^0.
|
|
87
|
-
"@mariozechner/pi-coding-agent": "^0.70.
|
|
88
|
-
"glimpseui": "^0.
|
|
84
|
+
"@juanibiapina/pi-powerbar": "^0.9.1",
|
|
85
|
+
"@mariozechner/pi-coding-agent": "^0.70.2",
|
|
86
|
+
"glimpseui": "^0.8.0",
|
|
89
87
|
"pi-diff-review": "file:./vendor/pi-diff-review",
|
|
90
|
-
"pi-interview": "^0.6
|
|
91
|
-
"pi-prompt-template-model": "^0.
|
|
88
|
+
"pi-interview": "^0.8.6",
|
|
89
|
+
"pi-prompt-template-model": "^0.9.1",
|
|
92
90
|
"pi-web-access": "^0.10.6",
|
|
93
91
|
"zod": "^4.3.6"
|
|
94
92
|
},
|
package/src/atomic.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
renameSync,
|
|
5
|
+
statSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import {
|
|
10
|
+
chmod,
|
|
11
|
+
mkdir,
|
|
12
|
+
rename,
|
|
13
|
+
stat,
|
|
14
|
+
unlink,
|
|
15
|
+
writeFile,
|
|
16
|
+
} from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
// Atomic write helpers. Writes to a sibling temp file in the same
|
|
20
|
+
// directory, then renames over the destination. POSIX rename(2) is
|
|
21
|
+
// atomic when the source and destination are on the same filesystem,
|
|
22
|
+
// so a reader (or a concurrent writer) never observes a partially
|
|
23
|
+
// written state file. On Windows the rename is also atomic when the
|
|
24
|
+
// destination exists.
|
|
25
|
+
//
|
|
26
|
+
// The temp suffix includes a random nonce so two concurrent writers
|
|
27
|
+
// to the same path don't collide on their temp files.
|
|
28
|
+
|
|
29
|
+
function tempPathFor(filePath: string): string {
|
|
30
|
+
return `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function existingMode(filePath: string): Promise<number | undefined> {
|
|
34
|
+
try {
|
|
35
|
+
return (await stat(filePath)).mode & 0o777;
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function existingModeSync(filePath: string): number | undefined {
|
|
42
|
+
try {
|
|
43
|
+
return statSync(filePath).mode & 0o777;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeFileAtomic(
|
|
50
|
+
filePath: string,
|
|
51
|
+
content: string | Uint8Array,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
54
|
+
const tempPath = tempPathFor(filePath);
|
|
55
|
+
const mode = await existingMode(filePath);
|
|
56
|
+
try {
|
|
57
|
+
await writeFile(tempPath, content, "utf8");
|
|
58
|
+
if (mode !== undefined) {
|
|
59
|
+
await chmod(tempPath, mode);
|
|
60
|
+
}
|
|
61
|
+
await rename(tempPath, filePath);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Best-effort cleanup of the orphaned temp file; ignore failure
|
|
64
|
+
// since the original error is what matters.
|
|
65
|
+
try {
|
|
66
|
+
await unlink(tempPath);
|
|
67
|
+
} catch {
|
|
68
|
+
/* temp may not exist */
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function writeFileAtomicSync(
|
|
75
|
+
filePath: string,
|
|
76
|
+
content: string | Uint8Array,
|
|
77
|
+
): void {
|
|
78
|
+
const tempPath = tempPathFor(filePath);
|
|
79
|
+
const mode = existingModeSync(filePath);
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(tempPath, content, "utf8");
|
|
82
|
+
if (mode !== undefined) {
|
|
83
|
+
chmodSync(tempPath, mode);
|
|
84
|
+
}
|
|
85
|
+
renameSync(tempPath, filePath);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
try {
|
|
88
|
+
unlinkSync(tempPath);
|
|
89
|
+
} catch {
|
|
90
|
+
/* temp may not exist */
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdir, readFile
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
4
5
|
import type { OmniConfig } from "./contracts.js";
|
|
5
6
|
import { AVAILABLE_MODELS } from "./providers.js";
|
|
6
7
|
|
|
@@ -84,7 +85,7 @@ export async function writeConfig(
|
|
|
84
85
|
): Promise<void> {
|
|
85
86
|
const configPath = path.join(rootDir, CONFIG_PATH);
|
|
86
87
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
87
|
-
await
|
|
88
|
+
await writeFileAtomic(configPath, renderConfigContent(config));
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export async function updateModelConfig(
|
package/src/header.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function centerIn(text: string, width: number): string {
|
|
|
40
40
|
return " ".repeat(pad) + text;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function
|
|
43
|
+
function loadVersion(): string {
|
|
44
44
|
try {
|
|
45
45
|
const pkgPath = path.resolve(
|
|
46
46
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -56,16 +56,19 @@ function readVersion(): string {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Read once at module load — package.json doesn't change while the
|
|
60
|
+
// process is running, so re-parsing on every render is wasted work.
|
|
61
|
+
const VERSION = loadVersion();
|
|
62
|
+
|
|
59
63
|
export function pickWelcome(): string {
|
|
60
64
|
return WELCOME_MESSAGES[Math.floor(Math.random() * WELCOME_MESSAGES.length)];
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export function renderHeader(theme: Theme): Text {
|
|
64
|
-
const version = readVersion();
|
|
65
68
|
const welcome = pickWelcome();
|
|
66
69
|
|
|
67
70
|
const logo = ASCII_LOGO.map((line) => brand(line)).join("\n");
|
|
68
|
-
const subtitleText = `— P I v${
|
|
71
|
+
const subtitleText = `— P I v${VERSION} —`;
|
|
69
72
|
const subtitle = theme.fg("muted", centerIn(subtitleText, LOGO_WIDTH));
|
|
70
73
|
const taglineText = "plan · build · verify";
|
|
71
74
|
const tagline = theme.fg("muted", centerIn(taglineText, LOGO_WIDTH));
|
package/src/planning.ts
CHANGED
|
@@ -162,6 +162,9 @@ function buildBootstrapTasks(repoSignals: RepoSignals): TaskBrief[] {
|
|
|
162
162
|
return tasks;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
const RELATION_OVERLAP_THRESHOLD = 0.34;
|
|
166
|
+
const RELATION_SMALL_SET_LIMIT = 3;
|
|
167
|
+
|
|
165
168
|
const RELATION_STOPWORDS = new Set([
|
|
166
169
|
"a",
|
|
167
170
|
"an",
|
|
@@ -245,13 +248,20 @@ export function isRequestRelated(
|
|
|
245
248
|
const overlap = [...currentTokens].filter((token) =>
|
|
246
249
|
previousTokens.has(token),
|
|
247
250
|
);
|
|
248
|
-
if (overlap.length
|
|
249
|
-
return
|
|
251
|
+
if (overlap.length === 0) {
|
|
252
|
+
return false;
|
|
250
253
|
}
|
|
251
254
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
// Compare against the smaller token set: short follow-up summaries
|
|
256
|
+
// ("auth bug fix") have very few tokens, so even one match is meaningful
|
|
257
|
+
// there. With more text on either side, demand a real overlap ratio so a
|
|
258
|
+
// single incidental shared word ("users", "config") doesn't keep an
|
|
259
|
+
// unrelated plan alive.
|
|
260
|
+
const smaller = Math.min(previousTokens.size, currentTokens.size);
|
|
261
|
+
if (smaller <= RELATION_SMALL_SET_LIMIT) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return overlap.length / smaller >= RELATION_OVERLAP_THRESHOLD;
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
export function createInitialSpec(
|
package/src/plans.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdir, readFile, unlink
|
|
1
|
+
import { mkdir, readFile, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
4
5
|
import type { PlanEntry, PlanStatus } from "./contracts.js";
|
|
5
6
|
import { OMNI_DIR } from "./contracts.js";
|
|
6
7
|
|
|
@@ -109,7 +110,7 @@ async function writeIndex(
|
|
|
109
110
|
entries: PlanEntry[],
|
|
110
111
|
): Promise<void> {
|
|
111
112
|
await ensurePlansDir(rootDir);
|
|
112
|
-
await
|
|
113
|
+
await writeFileAtomic(indexPath(rootDir), renderIndex(entries));
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export async function createPlan(
|
|
@@ -123,10 +124,9 @@ export async function createPlan(
|
|
|
123
124
|
const entry: PlanEntry = { id, title, status: "active", createdAt };
|
|
124
125
|
|
|
125
126
|
await ensurePlansDir(rootDir);
|
|
126
|
-
await
|
|
127
|
+
await writeFileAtomic(
|
|
127
128
|
planFilePath(rootDir, id),
|
|
128
129
|
renderPlanFile(entry, description, tasks),
|
|
129
|
-
"utf8",
|
|
130
130
|
);
|
|
131
131
|
|
|
132
132
|
const entries = await readPlanIndex(rootDir);
|
|
@@ -157,7 +157,7 @@ export async function updatePlanStatus(
|
|
|
157
157
|
const filePath = planFilePath(rootDir, planId);
|
|
158
158
|
const content = await readFile(filePath, "utf8");
|
|
159
159
|
const updated = content.replace(/^Status:\s*.+$/mu, `Status: ${status}`);
|
|
160
|
-
await
|
|
160
|
+
await writeFileAtomic(filePath, updated);
|
|
161
161
|
} catch {
|
|
162
162
|
// file may have been cleaned up already
|
|
163
163
|
}
|
|
@@ -198,13 +198,12 @@ export async function appendProgress(
|
|
|
198
198
|
|
|
199
199
|
try {
|
|
200
200
|
const content = await readFile(filePath, "utf8");
|
|
201
|
-
await
|
|
201
|
+
await writeFileAtomic(filePath, `${content.trimEnd()}\n${bullet}\n`);
|
|
202
202
|
} catch {
|
|
203
203
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
204
|
-
await
|
|
204
|
+
await writeFileAtomic(
|
|
205
205
|
filePath,
|
|
206
206
|
`# Progress\n\nOngoing log of project progress.\n\n${bullet}\n`,
|
|
207
|
-
"utf8",
|
|
208
207
|
);
|
|
209
208
|
}
|
|
210
209
|
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdirSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
rmSync,
|
|
6
|
-
writeFileSync,
|
|
7
|
-
} from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
8
2
|
import os from "node:os";
|
|
9
3
|
import path from "node:path";
|
|
10
4
|
|
|
5
|
+
import { writeFileAtomicSync } from "./atomic.js";
|
|
6
|
+
|
|
11
7
|
const MANAGED_PROMPT_FILES = ["commit.md", "push.md"] as const;
|
|
12
8
|
const MANAGED_SUBDIR = "omni-pi";
|
|
13
9
|
const LEGACY_MANAGED_SUBDIRS = ["zz-omni-pi"] as const;
|
|
@@ -55,7 +51,7 @@ export function ensureBundledPromptTemplates(
|
|
|
55
51
|
continue;
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
writeFileAtomicSync(targetPath, nextContent);
|
|
59
55
|
written.push(targetPath);
|
|
60
56
|
}
|
|
61
57
|
|