pi-mono-all 1.2.2 → 1.2.4

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # pi-mono-all
2
2
 
3
+ ## 1.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Bundle `pi-mono-sentinel@1.12.0` with project-scoped config stored under the Pi agent directory instead of the current working directory.
8
+
9
+ ## 1.2.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Bundle `pi-mono-linear@0.2.3` with Linear schema drift fixes and team-key resolution for issue creation.
14
+
3
15
  ## 1.2.2
4
16
 
5
17
  ### Patch Changes
@@ -1,5 +1,12 @@
1
1
  # pi-mono-linear
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Update Project GraphQL selections to use `teams` instead of removed `team` fields.
8
+ - Resolve `linear_create_issue` team keys to UUIDs before calling the Linear mutation and return clearer errors for unknown keys.
9
+
3
10
  ## 0.2.2
4
11
 
5
12
  ### Patch Changes
@@ -129,6 +129,7 @@ Linear API keys are sent in the `Authorization` header as the raw key value; do
129
129
  ## Usage tips
130
130
 
131
131
  - Use `linear_workspace_metadata` first when team/project/state/label/user IDs are unknown.
132
+ - `linear_create_issue` accepts either a team UUID or a team key; keys are resolved to UUIDs before the Linear mutation.
132
133
  - Use `linear_search_issues` for keyword lookup.
133
134
  - Use `linear_get_issue` before updating an issue or creating a comment.
134
135
  - Use `linear_list_issues` for filtered issue lists by team, assignee, status, and limit.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mono-linear",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Pi extension and skill for Linear GraphQL tools",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -68,6 +68,18 @@ interface FileUploadMutationResponse {
68
68
  };
69
69
  }
70
70
 
71
+ interface LinearTeamNode {
72
+ id: string;
73
+ name?: string;
74
+ key?: string;
75
+ }
76
+
77
+ interface ListTeamsResponse {
78
+ teams?: {
79
+ nodes?: LinearTeamNode[];
80
+ };
81
+ }
82
+
71
83
  export interface UploadedFileResult {
72
84
  filename: string;
73
85
  contentType: string;
@@ -134,8 +146,9 @@ export class LinearClient {
134
146
  return this.cached(`myIssues:${limit}`, () => this.graphql(queries.LIST_MY_ISSUES, { first: limit }));
135
147
  }
136
148
 
137
- createIssue(input: CreateIssueInput): Promise<unknown> {
138
- return this.graphql(queries.CREATE_ISSUE, { input: compact(input) });
149
+ async createIssue(input: CreateIssueInput): Promise<unknown> {
150
+ const teamId = await this.resolveTeamId(input.teamId);
151
+ return this.graphql(queries.CREATE_ISSUE, { input: compact({ ...input, teamId }) });
139
152
  }
140
153
 
141
154
  updateIssue(issueId: string, input: UpdateIssueInput): Promise<unknown> {
@@ -227,6 +240,24 @@ export class LinearClient {
227
240
  return this.cached(`document:${documentId}`, () => this.graphql(queries.GET_DOCUMENT, { id: documentId }));
228
241
  }
229
242
 
243
+ private async resolveTeamId(teamIdOrKey: string): Promise<string> {
244
+ const value = teamIdOrKey.trim();
245
+ if (!value) throw new ApiError("teamId is required", 400, undefined, "Linear");
246
+ if (isUuid(value)) return value;
247
+
248
+ const teams = await this.cached("teams", () => this.graphql<ListTeamsResponse>(queries.LIST_TEAMS));
249
+ const nodes = teams.teams?.nodes ?? [];
250
+ const match = nodes.find((team) => team.key?.toLowerCase() === value.toLowerCase());
251
+ if (match?.id) return match.id;
252
+
253
+ throw new ApiError(
254
+ `teamId must be a Linear team UUID or a known team key; "${value}" did not match any team key`,
255
+ 400,
256
+ { providedTeamId: value, availableTeamKeys: nodes.map((team) => team.key).filter(Boolean) },
257
+ "Linear",
258
+ );
259
+ }
260
+
230
261
  private async graphql<T = unknown>(query: string, variables: Variables = {}): Promise<T> {
231
262
  return this.limiter.schedule(async () => {
232
263
  const response = await this.http.post<GraphQlResponse<T>>("", { query, variables });
@@ -246,6 +277,10 @@ export function readLinearToken(): Promise<string> {
246
277
  return readAuthToken({ envName: "LINEAR_API_KEY", authPath: ["linear", "key"] });
247
278
  }
248
279
 
280
+ function isUuid(value: string): boolean {
281
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
282
+ }
283
+
249
284
  function buildIssueFilter(options: ListIssuesOptions): Variables {
250
285
  const filter: Variables = {};
251
286
  if (options.teamId) filter.team = { id: { eq: options.teamId } };
@@ -51,9 +51,9 @@ export const UPDATE_ISSUE = `mutation($id: String!, $input: IssueUpdateInput!) {
51
51
  issueUpdate(id: $id, input: $input) { success issue { id identifier title priority state { id name } } }
52
52
  }`;
53
53
 
54
- export const LIST_PROJECTS = `query { projects { nodes { id name description state team { id name key } } } }`;
54
+ export const LIST_PROJECTS = `query { projects { nodes { id name description state teams { nodes { id name key } } } } }`;
55
55
  export const LIST_TEAM_PROJECTS = `query($id: String!) { team(id: $id) { id name projects { nodes { id name description state } } } }`;
56
- export const GET_PROJECT = `query($id: String!) { project(id: $id) { id name description state url team { id name key } lead { id name } } }`;
56
+ export const GET_PROJECT = `query($id: String!) { project(id: $id) { id name description state url teams { nodes { id name key } } lead { id name } } }`;
57
57
 
58
58
  export const LIST_STATUSES = `query { workflowStates { nodes { id name type color position team { id name key } } } }`;
59
59
  export const LIST_TEAM_STATUSES = `query($id: String!) { team(id: $id) { id name states { nodes { id name type color position } } } }`;
@@ -94,7 +94,7 @@ export const GET_DOCUMENT = `query($id: String!) { document(id: $id) { id title
94
94
 
95
95
  export const WORKSPACE_METADATA = `query {
96
96
  teams { nodes { id name key } }
97
- projects { nodes { id name description state team { id name } } }
97
+ projects { nodes { id name description state teams { nodes { id name key } } } }
98
98
  workflowStates { nodes { id name type color position team { id name key } } }
99
99
  issueLabels { nodes { id name color team { id name key } } }
100
100
  users { nodes { id name email displayName } }
@@ -5,7 +5,7 @@ export const MaxResponseCharsSchema = Type.Optional(
5
5
  );
6
6
 
7
7
  export const LimitSchema = Type.Optional(Type.Number({ description: "Maximum number of records to fetch", minimum: 1, maximum: 250 }));
8
- export const TeamIdSchema = Type.String({ description: "Linear team UUID or key where accepted by Linear" });
8
+ export const TeamIdSchema = Type.String({ description: "Linear team UUID or key (keys are resolved to UUIDs for issue creation)" });
9
9
  export const IssueIdSchema = Type.String({ description: "Linear issue UUID or identifier such as ENG-123" });
10
10
  export const UserIdSchema = Type.String({ description: "Linear user UUID" });
11
11
  export const ProjectIdSchema = Type.String({ description: "Linear project UUID" });
@@ -157,7 +157,7 @@ export function registerLinearTools(pi: ExtensionAPI): void {
157
157
  pi.registerTool({
158
158
  name: "linear_create_issue",
159
159
  label: "Linear Create Issue",
160
- description: "Create a Linear issue. Use linear_workspace_metadata first if team/state/user/project IDs are unknown.",
160
+ description: "Create a Linear issue. Accepts a team UUID or team key; keys are resolved before calling Linear. Use linear_workspace_metadata first if team/state/user/project IDs are unknown.",
161
161
  parameters: LinearCreateIssueParams,
162
162
  async execute(_id, params, _signal, _onUpdate, ctx) {
163
163
  const result = await withLinearAuth(ctx, () => client.createIssue({
@@ -1,5 +1,11 @@
1
1
  # pi-mono-sentinel
2
2
 
3
+ ## 1.12.0
4
+
5
+ ### Changed
6
+
7
+ - Store project-scoped Sentinel config under the Pi agent directory (`~/.pi/agent/extensions/sentinel/projects/`) instead of creating `.pi/extensions/sentinel.json` in the current working directory. Existing cwd-local config files are still read for compatibility.
8
+
3
9
  ## 1.11.0
4
10
 
5
11
  ### Minor Changes
@@ -14,11 +14,13 @@ It addresses cross-cutting security gaps that pure command-based guardrails miss
14
14
  Sentinel reads and merges optional JSON config from three scopes:
15
15
 
16
16
  1. Global: `$PI_CODING_AGENT_DIR/extensions/sentinel.json` or `~/.pi/agent/extensions/sentinel.json`
17
- 2. Local/project: `.pi/extensions/sentinel.json` under the current working directory
17
+ 2. Local/project: a current-working-directory scoped file under `$PI_CODING_AGENT_DIR/extensions/sentinel/projects/` or `~/.pi/agent/extensions/sentinel/projects/`
18
18
  3. Memory: session-only grants written internally while Pi is running
19
19
 
20
20
  Merge priority is `memory > local > global > defaults`.
21
21
 
22
+ Local/project config is stored in Pi's agent directory instead of the user's working directory, so Sentinel does not create `.pi/` files in arbitrary project folders. Existing legacy `.pi/extensions/sentinel.json` files are still read for compatibility, but new local/project writes go to the agent directory.
23
+
22
24
  ```json
23
25
  {
24
26
  "enabled": true,
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { afterEach, beforeEach, describe, test } from "node:test";
6
6
 
7
7
  import { SentinelConfigLoader } from "../config.ts";
@@ -39,7 +39,7 @@ describe("SentinelConfigLoader", () => {
39
39
  mkdirSync(join(agentDir, "extensions"), { recursive: true });
40
40
  writeFileSync(loader.getConfigPath("global"), JSON.stringify({ pathAccess: { allowedPaths: ["/global"] } }), { flag: "w" });
41
41
  loader.load(cwd);
42
- mkdirSync(join(cwd, ".pi", "extensions"), { recursive: true });
42
+ mkdirSync(dirname(loader.getConfigPath("local")), { recursive: true });
43
43
  writeFileSync(loader.getConfigPath("local"), JSON.stringify({ features: { pathAccess: true }, pathAccess: { allowedPaths: ["/local"] } }), { flag: "w" });
44
44
  loader.load(cwd);
45
45
  loader.save("memory", { pathAccess: { mode: "block", allowedPaths: ["/memory"] } });
@@ -49,12 +49,26 @@ describe("SentinelConfigLoader", () => {
49
49
  assert.deepEqual(config.pathAccess.allowedPaths, ["/memory"]);
50
50
  });
51
51
 
52
- test("save writes global and local config files", () => {
52
+ test("save writes global and cwd-scoped local config files under the agent dir", () => {
53
53
  const loader = new SentinelConfigLoader();
54
54
  loader.load(cwd);
55
55
  loader.save("global", { enabled: false });
56
56
  loader.save("local", { features: { pathAccess: true } });
57
57
  assert.deepEqual(loader.getRawConfig("global"), { enabled: false });
58
58
  assert.deepEqual(loader.getRawConfig("local"), { features: { pathAccess: true } });
59
+ assert.equal(loader.getConfigPath("local").startsWith(join(agentDir, "extensions", "sentinel", "projects")), true);
60
+ assert.equal(existsSync(join(cwd, ".pi", "extensions", "sentinel.json")), false);
61
+ });
62
+
63
+ test("reads legacy cwd-local config without writing back to cwd", () => {
64
+ const loader = new SentinelConfigLoader();
65
+ mkdirSync(join(cwd, ".pi", "extensions"), { recursive: true });
66
+ writeFileSync(join(cwd, ".pi", "extensions", "sentinel.json"), JSON.stringify({ features: { pathAccess: true } }), { flag: "w" });
67
+
68
+ loader.load(cwd);
69
+ assert.equal(loader.getConfig().features.pathAccess, true);
70
+
71
+ loader.save("local", { pathAccess: { allowedPaths: ["/outside"] } });
72
+ assert.equal(existsSync(loader.getConfigPath("local")), true);
59
73
  });
60
74
  });
@@ -44,7 +44,10 @@ describe("path-access helpers", () => {
44
44
  });
45
45
 
46
46
  test("derives and persists selected path-access grants with the correct scope and target", () => {
47
+ const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
48
+ const agentDir = mkdtempSync(join(tmpdir(), "sentinel-path-access-agent-"));
47
49
  const cwd = mkdtempSync(join(tmpdir(), "sentinel-path-access-cwd-"));
50
+ process.env.PI_CODING_AGENT_DIR = agentDir;
48
51
  try {
49
52
  configLoader.load(cwd);
50
53
  configLoader.save("memory", { features: { pathAccess: true }, pathAccess: { mode: "ask", allowedPaths: [] } });
@@ -69,6 +72,9 @@ describe("path-access helpers", () => {
69
72
  configLoader.addAllowedPath(localFileGrant.scope, localFileGrant.grant);
70
73
  assert.ok(configLoader.getRawConfig("local")?.pathAccess?.allowedPaths?.includes("/tmp/outside-file.txt"));
71
74
  } finally {
75
+ if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
76
+ else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
77
+ rmSync(agentDir, { recursive: true, force: true });
72
78
  rmSync(cwd, { recursive: true, force: true });
73
79
  }
74
80
  });
@@ -1,6 +1,7 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
4
5
 
5
6
  export type SentinelConfigScope = "global" | "local" | "memory";
6
7
  export type SentinelPathAccessMode = "allow" | "ask" | "block";
@@ -92,6 +93,20 @@ function writeJson(path: string, value: SentinelConfig): void {
92
93
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
93
94
  }
94
95
 
96
+ function localScopeId(cwd: string): string {
97
+ const normalizedCwd = resolve(cwd);
98
+ const slug = (basename(normalizedCwd) || "root")
99
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
100
+ .replace(/^-+|-+$/g, "")
101
+ .slice(0, 48) || "root";
102
+ const hash = createHash("sha256").update(normalizedCwd).digest("hex").slice(0, 12);
103
+ return `${slug}-${hash}.json`;
104
+ }
105
+
106
+ function legacyLocalConfigPath(cwd: string): string {
107
+ return join(resolve(cwd), ".pi", "extensions", "sentinel.json");
108
+ }
109
+
95
110
  function isPlainObject(value: unknown): value is Record<string, unknown> {
96
111
  return typeof value === "object" && value !== null && !Array.isArray(value);
97
112
  }
@@ -127,7 +142,11 @@ export class SentinelConfigLoader {
127
142
  load(cwd = process.cwd()): void {
128
143
  this.localCwd = cwd;
129
144
  this.globalConfig = readJson(this.getConfigPath("global"));
130
- this.localConfig = readJson(this.getConfigPath("local"));
145
+ const legacyLocalConfig = readJson(legacyLocalConfigPath(this.localCwd));
146
+ const scopedLocalConfig = readJson(this.getConfigPath("local"));
147
+ this.localConfig = legacyLocalConfig && scopedLocalConfig
148
+ ? mergeConfig(legacyLocalConfig, scopedLocalConfig)
149
+ : scopedLocalConfig ?? legacyLocalConfig;
131
150
  this.resolvedConfig = undefined;
132
151
  }
133
152
 
@@ -152,7 +171,7 @@ export class SentinelConfigLoader {
152
171
  getConfigPath(scope: Exclude<SentinelConfigScope, "memory">): string {
153
172
  return scope === "global"
154
173
  ? join(getAgentDir(), "extensions", "sentinel.json")
155
- : join(this.localCwd, ".pi", "extensions", "sentinel.json");
174
+ : join(getAgentDir(), "extensions", "sentinel", "projects", localScopeId(this.localCwd));
156
175
  }
157
176
 
158
177
  save(scope: SentinelConfigScope, partial: SentinelConfig): void {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mono-sentinel",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Pi extension that guards against content-based secret leaks and indirect script execution",
5
5
  "keywords": [
6
6
  "pi-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mono-all",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "All pi-mono extensions and bundled skills",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -14,19 +14,19 @@
14
14
  "pi-mono-btw": "1.7.4",
15
15
  "pi-mono-clear": "1.7.3",
16
16
  "pi-mono-context": "0.1.1",
17
- "pi-mono-figma": "0.2.2",
18
- "pi-mono-linear": "0.2.2",
19
17
  "pi-mono-context-guard": "1.7.3",
20
- "pi-mono-loop": "1.7.3",
21
- "pi-mono-review": "1.8.2",
18
+ "pi-mono-linear": "0.2.3",
19
+ "pi-common": "0.1.1",
20
+ "pi-mono-figma": "0.2.2",
22
21
  "pi-mono-multi-edit": "1.7.3",
23
- "pi-mono-sentinel": "1.11.0",
22
+ "pi-mono-sentinel": "1.12.0",
24
23
  "pi-mono-simplify": "1.7.3",
24
+ "pi-mono-review": "1.8.2",
25
+ "pi-mono-team-mode": "2.3.2",
25
26
  "pi-mono-status-line": "1.7.3",
26
27
  "pi-mono-usage": "0.1.1",
27
28
  "pi-mono-web-search": "0.1.0",
28
- "pi-common": "0.1.1",
29
- "pi-mono-team-mode": "2.3.2"
29
+ "pi-mono-loop": "1.7.3"
30
30
  },
31
31
  "bundledDependencies": [
32
32
  "pi-mono-ask-user-question",