@nmzpy/pi-ember-stack 0.1.6 → 0.2.2

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.
Files changed (36) hide show
  1. package/README.md +83 -83
  2. package/package.json +62 -48
  3. package/plugins/devin-auth/extensions/index.ts +68 -7
  4. package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
  5. package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
  6. package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
  7. package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
  8. package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
  9. package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
  10. package/plugins/devin-auth/src/models.ts +42 -196
  11. package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
  12. package/plugins/devin-auth/src/oauth/types.ts +71 -71
  13. package/plugins/devin-auth/src/stream.ts +1 -1
  14. package/plugins/index.ts +29 -6
  15. package/plugins/pi-compact-tools/index.ts +35 -192
  16. package/plugins/pi-compact-tools/renderer.ts +589 -0
  17. package/plugins/pi-custom-agents/index.ts +727 -373
  18. package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
  19. package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
  20. package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
  21. package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
  22. package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
  23. package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
  24. package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
  25. package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
  26. package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
  27. package/plugins/pi-ember-fff/index.ts +975 -0
  28. package/plugins/pi-ember-fff/query.ts +247 -0
  29. package/plugins/pi-ember-fff/test/query.test.ts +222 -0
  30. package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
  31. package/plugins/pi-ember-tps/index.ts +144 -0
  32. package/plugins/pi-ember-ui/ember.json +99 -0
  33. package/plugins/pi-ember-ui/index.ts +805 -0
  34. package/plugins/pi-ember-ui/mode-colors.ts +196 -0
  35. package/tsconfig.json +2 -1
  36. package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
@@ -0,0 +1,247 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // External allowlist — auto-detected pi-coding-agent package directory
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /** The default alias for the pi-coding-agent package. */
9
+ export const PI_CODING_AGENT_ALIAS = "pi-coding-agent";
10
+
11
+ export type ExternalAllowlistEntry = {
12
+ /** User-facing alias, e.g. "pi-coding-agent" (no leading ./). */
13
+ alias: string;
14
+ /** Auto-detected absolute directory. */
15
+ dir: string;
16
+ };
17
+
18
+ export type ExternalAllowlist = {
19
+ entries: readonly ExternalAllowlistEntry[];
20
+ /** Resolve a user-facing path constraint to an allowlisted dir, or undefined. */
21
+ resolve(pathConstraint: string): ExternalAllowlistEntry | undefined;
22
+ /** True if an absolute path is inside an allowlisted dir. */
23
+ covers(absolutePath: string): boolean;
24
+ };
25
+
26
+ export type ExternalTarget = {
27
+ entry: ExternalAllowlistEntry;
28
+ /** Path relative to entry.dir, forward-slashed, no leading ./, or "" for the whole dir. */
29
+ relativePath: string;
30
+ };
31
+
32
+ /**
33
+ * Auto-detect the installed @earendil-works/pi-coding-agent package root.
34
+ * Returns undefined if detection fails (package not installed or
35
+ * import.meta.resolve unavailable).
36
+ */
37
+ function detect_pi_package_dir(): string | undefined {
38
+ try {
39
+ const resolved = import.meta.resolve("@earendil-works/pi-coding-agent");
40
+ const entryPath = fileURLToPath(resolved); // .../dist/index.js
41
+ return path.dirname(path.dirname(entryPath)); // package root (parent of dist/)
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Build the external allowlist by auto-detecting the installed
49
+ * @earendil-works/pi-coding-agent package directory. Returns an empty
50
+ * allowlist if detection fails (fail-safe: no external access).
51
+ */
52
+ export function buildExternalAllowlist(): ExternalAllowlist {
53
+ const dir = detect_pi_package_dir();
54
+ const entries: ExternalAllowlistEntry[] = [];
55
+ if (dir) {
56
+ entries.push({ alias: PI_CODING_AGENT_ALIAS, dir });
57
+ }
58
+ return {
59
+ entries,
60
+ resolve(pathConstraint: string): ExternalAllowlistEntry | undefined {
61
+ let trimmed = pathConstraint.trim();
62
+ if (!trimmed) return undefined;
63
+ if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
64
+ for (const entry of entries) {
65
+ if (trimmed === entry.alias) return entry;
66
+ if (
67
+ trimmed.startsWith(entry.alias + "/") ||
68
+ trimmed.startsWith(entry.alias + path.sep)
69
+ ) {
70
+ return entry;
71
+ }
72
+ }
73
+ return undefined;
74
+ },
75
+ covers(absolutePath: string): boolean {
76
+ for (const entry of entries) {
77
+ const rel = path.relative(entry.dir, absolutePath);
78
+ if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) {
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * If `pathConstraint` targets an allowlisted external directory, return
89
+ * the resolved entry and the sub-path relative to that dir. Otherwise
90
+ * undefined. This lets the tool layer route to a secondary FileFinder
91
+ * before normalization.
92
+ */
93
+ export function resolveExternalTarget(
94
+ pathConstraint: string | undefined,
95
+ allowlist: ExternalAllowlist,
96
+ ): ExternalTarget | undefined {
97
+ if (!pathConstraint) return undefined;
98
+ let trimmed = pathConstraint.trim();
99
+ if (!trimmed) return undefined;
100
+
101
+ // Absolute path — check if it's under an allowlisted dir.
102
+ if (path.isAbsolute(trimmed)) {
103
+ if (!allowlist.covers(trimmed)) return undefined;
104
+ for (const entry of allowlist.entries) {
105
+ const rel = path.relative(entry.dir, trimmed).replaceAll(path.sep, "/");
106
+ if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) {
107
+ return { entry, relativePath: rel };
108
+ }
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ // Strip leading ./
114
+ if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
115
+
116
+ // Alias-prefixed relative path
117
+ for (const entry of allowlist.entries) {
118
+ if (trimmed === entry.alias) {
119
+ return { entry, relativePath: "" };
120
+ }
121
+ // Check alias/ or alias\ as a prefix (cross-platform)
122
+ const aliasPrefixFwd = entry.alias + "/";
123
+ const aliasPrefixSep = entry.alias + path.sep;
124
+ if (trimmed.startsWith(aliasPrefixFwd) || trimmed.startsWith(aliasPrefixSep)) {
125
+ // Normalize to forward slashes for FFF
126
+ const remainder = trimmed.startsWith(aliasPrefixFwd)
127
+ ? trimmed.slice(aliasPrefixFwd.length)
128
+ : trimmed.slice(aliasPrefixSep.length).replaceAll(path.sep, "/");
129
+ return { entry, relativePath: remainder };
130
+ }
131
+ }
132
+
133
+ return undefined;
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Path constraint normalization
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export function normalizePathConstraint(
141
+ pathConstraint: string,
142
+ cwd = process.cwd(),
143
+ allowlist?: ExternalAllowlist,
144
+ ): string | null {
145
+ let trimmed = pathConstraint.trim();
146
+ if (!trimmed) return trimmed;
147
+
148
+ if (path.isAbsolute(trimmed)) {
149
+ // Allowlisted external absolute path — do not throw.
150
+ if (allowlist?.covers(trimmed)) {
151
+ const target = resolveExternalTarget(trimmed, allowlist);
152
+ if (target) {
153
+ // Return the relative path within the allowlisted dir.
154
+ // If bare dir, return null (whole dir). Otherwise return the
155
+ // relative subpath forward-slashed.
156
+ if (!target.relativePath) return null;
157
+ trimmed = target.relativePath;
158
+ // Jump to the post-absolute-path normalization logic below.
159
+ }
160
+ } else {
161
+ const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/");
162
+ if (relative === "") return null;
163
+ if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
164
+ throw new Error(
165
+ `Path constraint must be relative to the workspace: ${pathConstraint}`,
166
+ );
167
+ }
168
+ trimmed = relative;
169
+ }
170
+ } else if (allowlist) {
171
+ // Check for alias-prefixed relative path (e.g. ./pi-coding-agent/docs).
172
+ const target = resolveExternalTarget(trimmed, allowlist);
173
+ if (target) {
174
+ if (!target.relativePath) return null;
175
+ trimmed = target.relativePath;
176
+ }
177
+ }
178
+
179
+ if (trimmed === "." || trimmed === "./") return null;
180
+ // Strip a leading `./` so `./**/*.rs` and `**/*.rs` behave identically.
181
+ if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
182
+
183
+ // FFF's glob matcher can treat a hidden directory root glob such as
184
+ // `.agents/**` as empty, while the tool contract says this means "inside
185
+ // this directory". Collapse simple trailing recursive directory globs to the
186
+ // directory-prefix constraint understood by the parser. Keep real file globs
187
+ // such as `src/**/*.ts` unchanged.
188
+ const recursiveDir = trimmed.match(/^(.*)\/\*\*(?:\/\*)?$/);
189
+ if (recursiveDir) {
190
+ const dir = recursiveDir[1];
191
+ if (dir && !/[*?[{]/.test(dir)) return `${dir}/`;
192
+ }
193
+
194
+ // Already signals path-constraint syntax to the parser.
195
+ if (trimmed.startsWith("/") || trimmed.endsWith("/")) return trimmed;
196
+ // Globs (`*.ts`, `src/**/*.cc`, `{src,lib}`) are handled by the parser.
197
+ if (/[*?[{]/.test(trimmed)) return trimmed;
198
+ // Filename with extension (`main.rs`, `config.json`) → FilePath constraint.
199
+ const lastSegment = trimmed.split("/").pop() ?? "";
200
+ if (/\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSegment)) return trimmed;
201
+ // Bare directory prefix → append `/` so the parser sees a PathSegment.
202
+ return `${trimmed}/`;
203
+ }
204
+
205
+ // Exclusions are emitted as `!<constraint>` tokens, which the Rust parser
206
+ // understands (crates/fff-query-parser/src/parser.rs). We normalize each one
207
+ // the same way as the include path so bare dirs become PathSegment excludes.
208
+ // Tolerate callers passing already-negated forms like `!src/` by stripping
209
+ // the leading `!` before normalizing so we never double-negate (`!!src/`).
210
+ export function normalizeExcludes(
211
+ exclude: string | string[] | undefined,
212
+ cwd = process.cwd(),
213
+ allowlist?: ExternalAllowlist,
214
+ ): string[] {
215
+ if (!exclude) return [];
216
+ const list = Array.isArray(exclude) ? exclude : [exclude];
217
+ const out: string[] = [];
218
+ for (const raw of list) {
219
+ const parts = raw
220
+ .split(/[,\s]+/)
221
+ .map((s) => s.trim())
222
+ .filter(Boolean);
223
+ for (const p of parts) {
224
+ const stripped = p.startsWith("!") ? p.slice(1) : p;
225
+ const normalized = normalizePathConstraint(stripped, cwd, allowlist);
226
+ if (normalized) out.push(`!${normalized}`);
227
+ }
228
+ }
229
+ return out;
230
+ }
231
+
232
+ export function buildQuery(
233
+ path: string | undefined,
234
+ pattern: string,
235
+ exclude?: string | string[],
236
+ cwd = process.cwd(),
237
+ allowlist?: ExternalAllowlist,
238
+ ): string {
239
+ const parts: string[] = [];
240
+ if (path) {
241
+ const pathConstraint = normalizePathConstraint(path, cwd, allowlist);
242
+ if (pathConstraint) parts.push(pathConstraint);
243
+ }
244
+ parts.push(...normalizeExcludes(exclude, cwd, allowlist));
245
+ parts.push(pattern);
246
+ return parts.join(" ");
247
+ }
@@ -0,0 +1,222 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import {
4
+ normalizePathConstraint,
5
+ normalizeExcludes,
6
+ buildQuery,
7
+ buildExternalAllowlist,
8
+ resolveExternalTarget,
9
+ PI_CODING_AGENT_ALIAS,
10
+ } from "../query.ts";
11
+
12
+ describe("normalizePathConstraint", () => {
13
+ test("empty string returns empty", () => {
14
+ expect(normalizePathConstraint("")).toBe("");
15
+ });
16
+
17
+ test("dot returns null", () => {
18
+ expect(normalizePathConstraint(".")).toBeNull();
19
+ expect(normalizePathConstraint("./")).toBeNull();
20
+ });
21
+
22
+ test("bare directory gets trailing slash", () => {
23
+ expect(normalizePathConstraint("src")).toBe("src/");
24
+ });
25
+
26
+ test("glob is preserved", () => {
27
+ expect(normalizePathConstraint("*.ts")).toBe("*.ts");
28
+ expect(normalizePathConstraint("src/**/*.cc")).toBe("src/**/*.cc");
29
+ });
30
+
31
+ test("filename with extension is preserved", () => {
32
+ expect(normalizePathConstraint("main.rs")).toBe("main.rs");
33
+ });
34
+
35
+ test("recursive dir glob collapses to prefix", () => {
36
+ expect(normalizePathConstraint("src/**")).toBe("src/");
37
+ expect(normalizePathConstraint("src/**/*")).toBe("src/");
38
+ });
39
+
40
+ test("leading ./ is stripped", () => {
41
+ expect(normalizePathConstraint("./src")).toBe("src/");
42
+ });
43
+ });
44
+
45
+ describe("normalizeExcludes", () => {
46
+ test("undefined returns empty array", () => {
47
+ expect(normalizeExcludes(undefined)).toEqual([]);
48
+ });
49
+
50
+ test("comma-separated string is split", () => {
51
+ expect(normalizeExcludes("test/, *.min.js")).toEqual(["!test/", "!*.min.js"]);
52
+ });
53
+
54
+ test("leading ! is tolerated", () => {
55
+ expect(normalizeExcludes("!src/")).toEqual(["!src/"]);
56
+ });
57
+
58
+ test("array input works", () => {
59
+ expect(normalizeExcludes(["test/", "vendor/"])).toEqual(["!test/", "!vendor/"]);
60
+ });
61
+ });
62
+
63
+ describe("buildQuery", () => {
64
+ test("assembles path + excludes + pattern", () => {
65
+ const q = buildQuery("src/", "MyClass", "test/");
66
+ expect(q).toContain("src/");
67
+ expect(q).toContain("!test/");
68
+ expect(q).toContain("MyClass");
69
+ });
70
+
71
+ test("no path or excludes yields just pattern", () => {
72
+ expect(buildQuery(undefined, "foo")).toBe("foo");
73
+ });
74
+ });
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // ExternalAllowlist — auto-detection and resolveExternalTarget
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe("ExternalAllowlist", () => {
81
+ const allowlist = buildExternalAllowlist();
82
+ const has_entries = allowlist.entries.length > 0;
83
+ const cwd = process.cwd();
84
+
85
+ test("buildExternalAllowlist auto-detects the package dir", () => {
86
+ expect(allowlist.entries.length).toBeGreaterThanOrEqual(1);
87
+ expect(allowlist.entries[0].alias).toBe(PI_CODING_AGENT_ALIAS);
88
+ expect(path.isAbsolute(allowlist.entries[0].dir)).toBe(true);
89
+ });
90
+
91
+ test("resolveExternalTarget with ./pi-coding-agent alias → bare dir", () => {
92
+ if (!has_entries) return;
93
+ const target = resolveExternalTarget("./pi-coding-agent", allowlist);
94
+ expect(target).toBeDefined();
95
+ expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
96
+ expect(target!.relativePath).toBe("");
97
+ });
98
+
99
+ test("resolveExternalTarget with pi-coding-agent alias (no ./) → bare dir", () => {
100
+ if (!has_entries) return;
101
+ const target = resolveExternalTarget("pi-coding-agent", allowlist);
102
+ expect(target).toBeDefined();
103
+ expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
104
+ expect(target!.relativePath).toBe("");
105
+ });
106
+
107
+ test("resolveExternalTarget with ./pi-coding-agent/docs → subpath", () => {
108
+ if (!has_entries) return;
109
+ const target = resolveExternalTarget("./pi-coding-agent/docs", allowlist);
110
+ expect(target).toBeDefined();
111
+ expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
112
+ expect(target!.relativePath).toBe("docs");
113
+ });
114
+
115
+ test("resolveExternalTarget with ./pi-coding-agent/docs/extensions.md → deep subpath", () => {
116
+ if (!has_entries) return;
117
+ const target = resolveExternalTarget(
118
+ "./pi-coding-agent/docs/extensions.md",
119
+ allowlist,
120
+ );
121
+ expect(target).toBeDefined();
122
+ expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
123
+ expect(target!.relativePath).toBe("docs/extensions.md");
124
+ });
125
+
126
+ test("resolveExternalTarget with absolute path under allowlisted dir", () => {
127
+ if (!has_entries) return;
128
+ const abs = path.join(allowlist.entries[0].dir, "docs", "extensions.md");
129
+ const target = resolveExternalTarget(abs, allowlist);
130
+ expect(target).toBeDefined();
131
+ expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
132
+ expect(target!.relativePath).toBe("docs/extensions.md");
133
+ });
134
+
135
+ test("resolveExternalTarget returns undefined for workspace-relative src/", () => {
136
+ expect(resolveExternalTarget("src/", allowlist)).toBeUndefined();
137
+ });
138
+
139
+ test("resolveExternalTarget returns undefined for ./src", () => {
140
+ expect(resolveExternalTarget("./src", allowlist)).toBeUndefined();
141
+ });
142
+
143
+ test("resolveExternalTarget returns undefined for undefined input", () => {
144
+ expect(resolveExternalTarget(undefined, allowlist)).toBeUndefined();
145
+ });
146
+
147
+ test("resolveExternalTarget returns undefined for empty string", () => {
148
+ expect(resolveExternalTarget("", allowlist)).toBeUndefined();
149
+ });
150
+
151
+ test("resolveExternalTarget returns undefined for alias-prefix mismatch (pi-coding-agent-foo)", () => {
152
+ expect(resolveExternalTarget("pi-coding-agent-foo", allowlist)).toBeUndefined();
153
+ });
154
+ });
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // normalizePathConstraint with allowlist
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe("normalizePathConstraint with allowlist", () => {
161
+ const allowlist = buildExternalAllowlist();
162
+ const has_entries = allowlist.entries.length > 0;
163
+ const cwd = process.cwd();
164
+
165
+ test("allowlisted ./pi-coding-agent → null (bare dir)", () => {
166
+ if (!has_entries) return;
167
+ expect(normalizePathConstraint("./pi-coding-agent", cwd, allowlist)).toBeNull();
168
+ });
169
+
170
+ test("allowlisted ./pi-coding-agent/docs → docs/ (trailing slash)", () => {
171
+ if (!has_entries) return;
172
+ expect(normalizePathConstraint("./pi-coding-agent/docs", cwd, allowlist)).toBe("docs/");
173
+ });
174
+
175
+ test("allowlisted ./pi-coding-agent/docs/extensions.md → preserved filename", () => {
176
+ if (!has_entries) return;
177
+ expect(
178
+ normalizePathConstraint("./pi-coding-agent/docs/extensions.md", cwd, allowlist),
179
+ ).toBe("docs/extensions.md");
180
+ });
181
+
182
+ test("allowlisted absolute path under package dir → relative with trailing slash", () => {
183
+ if (!has_entries) return;
184
+ const abs = path.join(allowlist.entries[0].dir, "docs");
185
+ expect(normalizePathConstraint(abs, cwd, allowlist)).toBe("docs/");
186
+ });
187
+
188
+ test("non-allowlisted absolute path /etc/passwd → throws", () => {
189
+ expect(() => normalizePathConstraint("/etc/passwd", cwd, allowlist)).toThrow(
190
+ "Path constraint must be relative to the workspace: /etc/passwd",
191
+ );
192
+ });
193
+
194
+ test("workspace-relative src → src/ (unchanged with allowlist)", () => {
195
+ expect(normalizePathConstraint("src", cwd, allowlist)).toBe("src/");
196
+ });
197
+
198
+ test("workspace-relative glob *.ts → *.ts (unchanged with allowlist)", () => {
199
+ expect(normalizePathConstraint("*.ts", cwd, allowlist)).toBe("*.ts");
200
+ });
201
+
202
+ test("workspace-relative dot → null (unchanged with allowlist)", () => {
203
+ expect(normalizePathConstraint(".", cwd, allowlist)).toBeNull();
204
+ });
205
+ });
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // buildQuery with external target
209
+ // ---------------------------------------------------------------------------
210
+
211
+ describe("buildQuery with external target", () => {
212
+ const allowlist = buildExternalAllowlist();
213
+ const has_entries = allowlist.entries.length > 0;
214
+ const cwd = process.cwd();
215
+
216
+ test("buildQuery with ./pi-coding-agent/docs includes docs/ and pattern", () => {
217
+ if (!has_entries) return;
218
+ const q = buildQuery("./pi-coding-agent/docs", "renderCall", undefined, cwd, allowlist);
219
+ expect(q).toContain("docs/");
220
+ expect(q).toContain("renderCall");
221
+ });
222
+ });