@dahrk/linear 0.1.0 → 0.1.1

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 (62) hide show
  1. package/README.md +24 -12
  2. package/dist/comments.d.ts +36 -0
  3. package/dist/comments.d.ts.map +1 -0
  4. package/dist/comments.js +104 -0
  5. package/dist/comments.js.map +1 -0
  6. package/dist/documents.d.ts +1 -15
  7. package/dist/documents.d.ts.map +1 -1
  8. package/dist/documents.js +37 -27
  9. package/dist/documents.js.map +1 -1
  10. package/dist/format-action.d.ts +25 -0
  11. package/dist/format-action.d.ts.map +1 -0
  12. package/dist/format-action.js +250 -0
  13. package/dist/format-action.js.map +1 -0
  14. package/dist/index.d.ts +113 -11
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +182 -58
  17. package/dist/index.js.map +1 -1
  18. package/dist/issue-graph.d.ts +37 -0
  19. package/dist/issue-graph.d.ts.map +1 -0
  20. package/dist/issue-graph.js +125 -0
  21. package/dist/issue-graph.js.map +1 -0
  22. package/dist/issues.d.ts +3 -2
  23. package/dist/issues.d.ts.map +1 -1
  24. package/dist/issues.js +5 -10
  25. package/dist/issues.js.map +1 -1
  26. package/dist/labels.d.ts +47 -0
  27. package/dist/labels.d.ts.map +1 -1
  28. package/dist/labels.js +71 -23
  29. package/dist/labels.js.map +1 -1
  30. package/dist/linear-client.d.ts +51 -0
  31. package/dist/linear-client.d.ts.map +1 -1
  32. package/dist/linear-client.js +234 -34
  33. package/dist/linear-client.js.map +1 -1
  34. package/dist/oauth.d.ts +11 -10
  35. package/dist/oauth.d.ts.map +1 -1
  36. package/dist/oauth.js +35 -32
  37. package/dist/oauth.js.map +1 -1
  38. package/dist/recording-client.d.ts +20 -2
  39. package/dist/recording-client.d.ts.map +1 -1
  40. package/dist/recording-client.js +39 -1
  41. package/dist/recording-client.js.map +1 -1
  42. package/dist/responding-client.d.ts +49 -0
  43. package/dist/responding-client.d.ts.map +1 -0
  44. package/dist/responding-client.js +47 -0
  45. package/dist/responding-client.js.map +1 -0
  46. package/dist/teams.d.ts +20 -0
  47. package/dist/teams.d.ts.map +1 -0
  48. package/dist/teams.js +32 -0
  49. package/dist/teams.js.map +1 -0
  50. package/package.json +8 -10
  51. package/src/comments.ts +126 -0
  52. package/src/documents.ts +162 -0
  53. package/src/format-action.ts +279 -0
  54. package/src/index.ts +556 -0
  55. package/src/issue-graph.ts +169 -0
  56. package/src/issues.ts +93 -0
  57. package/src/labels.ts +254 -0
  58. package/src/linear-client.ts +449 -0
  59. package/src/oauth.ts +194 -0
  60. package/src/recording-client.ts +141 -0
  61. package/src/responding-client.ts +106 -0
  62. package/src/teams.ts +44 -0
@@ -1 +1 @@
1
- {"version":3,"file":"recording-client.d.ts","sourceRoot":"","sources":["../src/recording-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;CAAE,GACzF;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,YAAY,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,aAAa,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC;AAEjE,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,iCAAiC;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC;CAChC;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAkDvD"}
1
+ {"version":3,"file":"recording-client.d.ts","sourceRoot":"","sources":["../src/recording-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;CAAE,GACzF;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,YAAY,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,aAAa,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,iCAAiC;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC;CAChC;AAED,0FAA0F;AAC1F,MAAM,WAAW,sBAAsB;IACrC,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;0GAEsG;IACtG,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC,CAAC;CACrD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,sBAA2B,GAAG,eAAe,CAqFxF"}
@@ -1,5 +1,7 @@
1
- export function createRecordingClient() {
1
+ export function createRecordingClient(opts = {}) {
2
2
  const calls = [];
3
+ const appUserId = opts.appUserId ?? "app-user";
4
+ const engagement = opts.engagement ?? {};
3
5
  return {
4
6
  calls,
5
7
  async postActivity(sessionId, activity) {
@@ -25,9 +27,13 @@ export function createRecordingClient() {
25
27
  },
26
28
  async startIssue(sessionId) {
27
29
  calls.push({ call: "startIssue", sessionId });
30
+ // Offline stub: report a successful move so the reporter's no-op surfacing stays quiet.
31
+ return { moved: true, stateName: "In Progress" };
28
32
  },
29
33
  async moveIssueToReview(sessionId) {
30
34
  calls.push({ call: "moveIssueToReview", sessionId });
35
+ // Offline stub: report a successful move so the reporter's no-op surfacing stays quiet.
36
+ return { moved: true, stateName: "In Review" };
31
37
  },
32
38
  async commentOnIssue(sessionId, body) {
33
39
  calls.push({ call: "commentOnIssue", sessionId, body });
@@ -47,6 +53,38 @@ export function createRecordingClient() {
47
53
  async setState(sessionId, state) {
48
54
  calls.push({ call: "setState", sessionId, state });
49
55
  },
56
+ async readIssueEngagement(issueIds) {
57
+ calls.push({ call: "readIssueEngagement", issueIds });
58
+ const out = new Map();
59
+ for (const id of issueIds) {
60
+ if (!id)
61
+ continue;
62
+ if (Object.prototype.hasOwnProperty.call(engagement, id)) {
63
+ const e = engagement[id];
64
+ // null => hard-deleted: leave it out of the result entirely.
65
+ if (e)
66
+ out.set(id, e);
67
+ }
68
+ else {
69
+ // Default: fully engaged (delegated + assigned to the app user), so a sweep against an
70
+ // unconfigured recording client reads every issue as still-engaged.
71
+ out.set(id, { present: true, archived: false, stateType: "started", delegateId: appUserId, assigneeId: appUserId });
72
+ }
73
+ }
74
+ return out;
75
+ },
76
+ async appUserId() {
77
+ return appUserId;
78
+ },
79
+ async createSessionOnIssue(issueId) {
80
+ calls.push({ call: "createSessionOnIssue", issueId });
81
+ // Deterministic synthetic session id so offline runs and tests have a stable reference.
82
+ return `sess-${issueId}`;
83
+ },
84
+ async createSessionOnComment(commentId) {
85
+ calls.push({ call: "createSessionOnComment", commentId });
86
+ return `sess-${commentId}`;
87
+ },
50
88
  };
51
89
  }
52
90
  //# sourceMappingURL=recording-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recording-client.js","sourceRoot":"","sources":["../src/recording-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recording-client.js","sourceRoot":"","sources":["../src/recording-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * An actively-responding, credential-free AgentSessionClient for the `pnpm dev` offline
3
+ * environment. It is a superset of the recording client (./recording-client.ts): it keeps
4
+ * the ordered `calls` log so it is a drop-in wherever the recording client is injected, but
5
+ * it additionally
6
+ *
7
+ * 1. maintains a small, queryable per-session projection of what the hub last pushed
8
+ * (latest state, latest plan, appended activities, set external URLs), so a dev can
9
+ * observe a run without touching Linear, and
10
+ * 2. exposes a pure, pre-declared gate-decision policy (`decide`) a dev CLI reads to know
11
+ * which answer to fire for a stage.
12
+ *
13
+ * The gate policy is a value the dev supplies up front; it NEVER chooses the next stage
14
+ * (that would cross the determinism boundary). Gate answers remain human-drive: the CLI
15
+ * uses `decide` only to pick which `prompted` webhook to send.
16
+ */
17
+ import type { Activity, ElicitOption, ExternalUrl, PlanItem, SessionState } from "./index.js";
18
+ import { type RecordingClient, type RecordingClientOptions } from "./recording-client.js";
19
+ /** A pre-declared gate answer for a stage. Never a control-flow decision (see file header). */
20
+ export type GateDecision = "allow" | "deny";
21
+ /** What the hub last pushed for one session, observable offline without touching Linear. */
22
+ export interface SessionProjection {
23
+ /** The latest session state the hub mirrored, or undefined if none was set yet. */
24
+ state?: SessionState;
25
+ /** The latest plan (agent-plan checklist); replaced wholesale on each `setPlan`. */
26
+ plan: PlanItem[];
27
+ /** Every activity posted to the session, in order. */
28
+ activities: Activity[];
29
+ /** The latest external-URL set; replaced wholesale on each `setExternalUrls`. */
30
+ externalUrls: ExternalUrl[];
31
+ /** The latest elicitation raised on the session, if any. */
32
+ elicitation?: {
33
+ prompt: string;
34
+ options?: ElicitOption[];
35
+ };
36
+ }
37
+ export interface RespondingClientOptions extends RecordingClientOptions {
38
+ /** Per-stage gate answers the dev pre-declares. A stage absent here defaults to "allow". */
39
+ gateDecisions?: Record<string, GateDecision>;
40
+ }
41
+ export interface RespondingClient extends RecordingClient {
42
+ /** The queryable projection of what the hub last pushed for a session, or undefined if the
43
+ * hub has not touched that session. */
44
+ session(sessionId: string): SessionProjection | undefined;
45
+ /** The dev's pre-declared gate answer for a stage (default "allow"). Pure. */
46
+ decide(stageId: string): GateDecision;
47
+ }
48
+ export declare function createRespondingLinearClient(opts?: RespondingClientOptions): RespondingClient;
49
+ //# sourceMappingURL=responding-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responding-client.d.ts","sourceRoot":"","sources":["../src/responding-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,YAAY,EACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B,+FAA+F;AAC/F,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;AAE5C,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IAChC,mFAAmF;IACnF,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,oFAAoF;IACpF,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,sDAAsD;IACtD,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvB,iFAAiF;IACjF,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,4DAA4D;IAC5D,WAAW,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;CAC5D;AAED,MAAM,WAAW,uBAAwB,SAAQ,sBAAsB;IACrE,4FAA4F;IAC5F,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD;4CACwC;IACxC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAAC;IAC1D,8EAA8E;IAC9E,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;CACvC;AAED,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,uBAA4B,GAAG,gBAAgB,CA8CjG"}
@@ -0,0 +1,47 @@
1
+ import { createRecordingClient, } from "./recording-client.js";
2
+ export function createRespondingLinearClient(opts = {}) {
3
+ // Reuse the recording client wholesale for the ordered call log and the seeded read-side
4
+ // methods (engagement, appUserId, createDocument), then wrap the mutating methods to also
5
+ // update a per-session projection.
6
+ const base = createRecordingClient(opts);
7
+ const gateDecisions = opts.gateDecisions ?? {};
8
+ const projections = new Map();
9
+ const projectionFor = (sessionId) => {
10
+ let p = projections.get(sessionId);
11
+ if (!p) {
12
+ p = { plan: [], activities: [], externalUrls: [] };
13
+ projections.set(sessionId, p);
14
+ }
15
+ return p;
16
+ };
17
+ return {
18
+ ...base,
19
+ async setPlan(sessionId, items) {
20
+ await base.setPlan(sessionId, items);
21
+ projectionFor(sessionId).plan = items;
22
+ },
23
+ async postActivity(sessionId, activity) {
24
+ await base.postActivity(sessionId, activity);
25
+ projectionFor(sessionId).activities.push(activity);
26
+ },
27
+ async raiseElicitation(sessionId, prompt, options) {
28
+ await base.raiseElicitation(sessionId, prompt, options);
29
+ projectionFor(sessionId).elicitation = { prompt, ...(options ? { options } : {}) };
30
+ },
31
+ async setExternalUrls(sessionId, urls) {
32
+ await base.setExternalUrls(sessionId, urls);
33
+ projectionFor(sessionId).externalUrls = urls;
34
+ },
35
+ async setState(sessionId, state) {
36
+ await base.setState(sessionId, state);
37
+ projectionFor(sessionId).state = state;
38
+ },
39
+ session(sessionId) {
40
+ return projections.get(sessionId);
41
+ },
42
+ decide(stageId) {
43
+ return gateDecisions[stageId] ?? "allow";
44
+ },
45
+ };
46
+ }
47
+ //# sourceMappingURL=responding-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responding-client.js","sourceRoot":"","sources":["../src/responding-client.ts"],"names":[],"mappings":"AAuBA,OAAO,EACL,qBAAqB,GAGtB,MAAM,uBAAuB,CAAC;AAgC/B,MAAM,UAAU,4BAA4B,CAAC,OAAgC,EAAE;IAC7E,yFAAyF;IACzF,0FAA0F;IAC1F,mCAAmC;IACnC,MAAM,IAAI,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC;IAC/C,MAAM,WAAW,GAAG,IAAI,GAAG,EAA6B,CAAC;IAEzD,MAAM,aAAa,GAAG,CAAC,SAAiB,EAAqB,EAAE;QAC7D,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;YACnD,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IAEF,OAAO;QACL,GAAG,IAAI;QACP,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK;YAC5B,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACrC,aAAa,CAAC,SAAS,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC;QACxC,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ;YACpC,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YAC7C,aAAa,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrD,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO;YAC/C,MAAM,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YACxD,aAAa,CAAC,SAAS,CAAC,CAAC,WAAW,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;QACD,KAAK,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI;YACnC,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,aAAa,CAAC,SAAS,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK;YAC7B,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACtC,aAAa,CAAC,SAAS,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC;QACzC,CAAC;QACD,OAAO,CAAC,SAAS;YACf,OAAO,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,CAAC,OAAO;YACZ,OAAO,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ /** A workspace team, reduced to just what the picker needs. */
2
+ export interface Team {
3
+ id: string;
4
+ key: string;
5
+ name: string;
6
+ }
7
+ /** The injectable seam: list the workspace's teams. */
8
+ export interface TeamsApi {
9
+ listTeams(): Promise<Team[]>;
10
+ }
11
+ /**
12
+ * List the workspace's teams via the `TeamsApi` seam, normalised to `{id,key,name}` and sorted by
13
+ * key so the picker renders in a stable, deterministic order. A thin deterministic transform - no
14
+ * control flow, no network - so it feeds the portal picker directly.
15
+ */
16
+ export declare function listWorkspaceTeams(api: TeamsApi): Promise<Team[]>;
17
+ /** The live `TeamsApi` backed by a Linear bearer token (workspace-scoped teams). Paginates
18
+ * `client.teams()` exactly as `linearLabelApi` paginates `issueLabels()`. */
19
+ export declare function linearTeamsApi(token: string): TeamsApi;
20
+ //# sourceMappingURL=teams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams.d.ts","sourceRoot":"","sources":["../src/teams.ts"],"names":[],"mappings":"AAQA,+DAA+D;AAC/D,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uDAAuD;AACvD,MAAM,WAAW,QAAQ;IACvB,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAKvE;AAED;8EAC8E;AAC9E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAStD"}
package/dist/teams.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Workspace-team listing (DHK-777). A repository routes issues to itself partly by team key
3
+ * (`Repository.routing.teamKeys`); the portal offers the real keys instead of free text by reading
4
+ * the linked workspace's teams. The `TeamsApi` seam keeps this pure and unit-testable: the live
5
+ * adapter (`linearTeamsApi`) wraps `@linear/sdk`, while tests inject a fake - mirroring `LabelApi`.
6
+ */
7
+ import { LinearClient } from "@linear/sdk";
8
+ /**
9
+ * List the workspace's teams via the `TeamsApi` seam, normalised to `{id,key,name}` and sorted by
10
+ * key so the picker renders in a stable, deterministic order. A thin deterministic transform - no
11
+ * control flow, no network - so it feeds the portal picker directly.
12
+ */
13
+ export async function listWorkspaceTeams(api) {
14
+ const teams = await api.listTeams();
15
+ return teams
16
+ .map((t) => ({ id: t.id, key: t.key, name: t.name }))
17
+ .sort((a, b) => a.key.localeCompare(b.key));
18
+ }
19
+ /** The live `TeamsApi` backed by a Linear bearer token (workspace-scoped teams). Paginates
20
+ * `client.teams()` exactly as `linearLabelApi` paginates `issueLabels()`. */
21
+ export function linearTeamsApi(token) {
22
+ const client = new LinearClient({ accessToken: token });
23
+ return {
24
+ async listTeams() {
25
+ const conn = await client.teams();
26
+ while (conn.pageInfo.hasNextPage)
27
+ await conn.fetchNext();
28
+ return conn.nodes.map((t) => ({ id: t.id, key: t.key, name: t.name }));
29
+ },
30
+ };
31
+ }
32
+ //# sourceMappingURL=teams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams.js","sourceRoot":"","sources":["../src/teams.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAa;IACpD,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,SAAS,EAAE,CAAC;IACpC,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;SACpD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;8EAC8E;AAC9E,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,OAAO;QACL,KAAK,CAAC,SAAS;YACb,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,WAAW;gBAAE,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;YACzD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzE,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@dahrk/linear",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "The Linear Agent Session API client and webhook verification. The native control surface: activities, agent-plan checklist, elicitation gates, prompted-drive, stop signal, externalUrls.",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/skakel/dahrk.git"
10
- },
11
- "homepage": "https://github.com/skakel/dahrk#readme",
7
+ "homepage": "https://dahrk.ai",
12
8
  "bugs": {
13
- "url": "https://github.com/skakel/dahrk/issues"
9
+ "url": "https://github.com/dahrkai/dahrk-node/issues"
14
10
  },
15
11
  "exports": {
16
12
  ".": {
@@ -19,14 +15,15 @@
19
15
  }
20
16
  },
21
17
  "files": [
22
- "dist"
18
+ "dist",
19
+ "src"
23
20
  ],
24
21
  "publishConfig": {
25
22
  "access": "public"
26
23
  },
27
24
  "dependencies": {
28
25
  "@linear/sdk": "^86.0.0",
29
- "@dahrk/contracts": "0.1.0"
26
+ "@dahrk/contracts": "0.11.1"
30
27
  },
31
28
  "devDependencies": {
32
29
  "tsx": "^4.19.0"
@@ -35,7 +32,8 @@
35
32
  "build": "tsc -p tsconfig.json",
36
33
  "typecheck": "tsc -p tsconfig.json --noEmit",
37
34
  "test": "node --import tsx --test test/*.test.ts",
38
- "spike/live": "tsx spike/run.ts"
35
+ "spike/live": "tsx spike/run.ts",
36
+ "spike/filter": "tsx spike/filter-probe.ts"
39
37
  },
40
38
  "main": "./dist/index.js",
41
39
  "types": "./dist/index.d.ts"
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Fetch the comment thread on a Linear issue, so the hub can snapshot it into a run and surface it to
3
+ * the agent. Until now nothing in the harness read comment bodies at all: an agent saw only whatever
4
+ * Linear happened to inline in the webhook's `promptContext`, so the conversation that actually
5
+ * decided the shape of a ticket was invisible to the stage doing the work.
6
+ *
7
+ * The one piece of logic that is not a plain read: the app user's OWN comments are dropped. The
8
+ * harness posts its stage summaries back through `commentOnIssue`, so without this filter every stage
9
+ * would read the previous stage's summary as though a human had written it - the run talking to itself,
10
+ * with the noise compounding on each continuation.
11
+ *
12
+ * The `CommentSource` seam keeps the assembly pure and unit-testable: the live source
13
+ * (`linearCommentSource`) speaks GraphQL, while tests inject a fake.
14
+ */
15
+ import { LinearClient } from "@linear/sdk";
16
+ import type { IssueComment } from "@dahrk/contracts";
17
+
18
+ /** A comment as returned by Linear before normalisation. */
19
+ export interface RawComment {
20
+ id: string;
21
+ body?: string;
22
+ createdAt?: Date | string;
23
+ /** Author user id, compared against the app user id to drop the harness's own posts. */
24
+ authorId?: string;
25
+ /** Author display name, surfaced to the agent. */
26
+ authorName?: string;
27
+ }
28
+
29
+ /** The read seam the assembly depends on; the live impl wraps GraphQL, tests inject a fake. */
30
+ export interface CommentSource {
31
+ /** The issue's comments, in whatever order Linear returns them. */
32
+ issueComments(issueId: string): Promise<RawComment[]>;
33
+ /** The id of the app user this token authenticates as, or null when it cannot be resolved. */
34
+ appUserId(): Promise<string | null>;
35
+ }
36
+
37
+ /** Normalise a timestamp Linear may hand back as a Date or a string into ISO 8601. */
38
+ function isoOf(value: Date | string | undefined): string {
39
+ if (value instanceof Date) return value.toISOString();
40
+ if (typeof value === "string" && value.trim()) {
41
+ const parsed = new Date(value);
42
+ return Number.isNaN(parsed.getTime()) ? "" : parsed.toISOString();
43
+ }
44
+ return "";
45
+ }
46
+
47
+ /**
48
+ * Collect an issue's comments from a source: drop the app user's own posts, drop empty bodies, and
49
+ * order oldest first so the thread reads as a conversation. Pure of any network - the source does the
50
+ * I/O.
51
+ *
52
+ * Comments with no resolvable timestamp sort last rather than being dropped: an unorderable comment is
53
+ * still content worth showing, and losing it silently would be worse than showing it out of order.
54
+ */
55
+ export async function collectIssueComments(
56
+ source: CommentSource,
57
+ issueId: string,
58
+ ): Promise<IssueComment[]> {
59
+ const [raw, appUserId] = await Promise.all([source.issueComments(issueId), source.appUserId()]);
60
+ const out: IssueComment[] = [];
61
+ for (const comment of raw) {
62
+ if (appUserId && comment.authorId === appUserId) continue;
63
+ const body = (comment.body ?? "").trim();
64
+ if (!body) continue;
65
+ out.push({
66
+ id: comment.id,
67
+ author: (comment.authorName ?? "").trim(),
68
+ createdAt: isoOf(comment.createdAt),
69
+ body,
70
+ });
71
+ }
72
+ return out.sort((a, b) => {
73
+ if (!a.createdAt) return 1;
74
+ if (!b.createdAt) return -1;
75
+ return a.createdAt.localeCompare(b.createdAt);
76
+ });
77
+ }
78
+
79
+ /** The live `CommentSource`, backed by a Linear bearer token. Uses the typed SDK. */
80
+ export function linearCommentSource(token: string): CommentSource {
81
+ const client = new LinearClient({ accessToken: token });
82
+ return {
83
+ async issueComments(issueId) {
84
+ // Bounded, unpaginated cap, matching `linearDocumentSource.issueDocuments`: a thread longer than
85
+ // 100 comments silently drops the overflow. Deliberate, not a bug - the prompt inlines only the
86
+ // most recent few thousand characters anyway. Revisit with cursor pagination if it bites.
87
+ const issue = await client.issue(issueId);
88
+ const conn = await issue.comments({ first: 100 });
89
+ const nodes = await Promise.all(
90
+ conn.nodes.map(async (c) => {
91
+ // `user` is a lazy reference on the SDK's Comment; resolving it per comment is what lets us
92
+ // both filter the app user's posts and name the human author. A comment whose author does not
93
+ // resolve (a deleted user, an integration post) still rides through, unattributed.
94
+ const user = await c.user;
95
+ return {
96
+ id: c.id,
97
+ body: c.body,
98
+ createdAt: c.createdAt,
99
+ ...(user?.id ? { authorId: user.id } : {}),
100
+ ...(user?.displayName || user?.name ? { authorName: user.displayName ?? user.name } : {}),
101
+ } satisfies RawComment;
102
+ }),
103
+ );
104
+ return nodes;
105
+ },
106
+ async appUserId() {
107
+ try {
108
+ const viewer = await client.viewer;
109
+ return viewer?.id ?? null;
110
+ } catch {
111
+ // Failing to resolve the app user must not lose the thread. We return null and accept that the
112
+ // harness's own comments ride through this once, rather than dropping every comment.
113
+ return null;
114
+ }
115
+ },
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Fetch an issue's comment thread as contract `IssueComment`s, ready to snapshot into a run. The
121
+ * single entry point the hub calls. A hard failure (auth, network) propagates so the caller can log
122
+ * and proceed with none.
123
+ */
124
+ export async function fetchIssueComments(token: string, issueId: string): Promise<IssueComment[]> {
125
+ return collectIssueComments(linearCommentSource(token), issueId);
126
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Fetch the Linear Documents attached to an issue, so the hub can snapshot their content into a run
3
+ * and surface it to the agent (the harness was webhook-payload-only before, so a document reached the
4
+ * agent only as a URL, never as content). This is a read against the same per-connection Linear token
5
+ * the hub already uses to post back; fetching content is data assembly, not control flow.
6
+ *
7
+ * Two sources, de-duped by document id:
8
+ * 1. Documents directly associated with the issue (`issue.documents`).
9
+ * 2. Documents linked via the issue's attachments whose `url` is a Linear document URL
10
+ * (`https://linear.app/<ws>/document/<...>-<slug>`), resolved through `document(id:)`.
11
+ *
12
+ * The `DocumentSource` seam keeps the assembly pure and unit-testable: the live source
13
+ * (`linearDocumentSource`) speaks GraphQL, while tests inject a fake.
14
+ */
15
+ import { LinearClient } from "@linear/sdk";
16
+ import type { AttachedDocument } from "@dahrk/contracts";
17
+
18
+ /** A document as returned by Linear before slug normalisation (slugId is Linear's URL-safe slug). */
19
+ export interface RawDocument {
20
+ id: string;
21
+ slugId?: string;
22
+ title?: string;
23
+ url?: string;
24
+ content?: string;
25
+ }
26
+
27
+ /** The read seam the assembly depends on; the live impl wraps GraphQL, tests inject a fake. */
28
+ export interface DocumentSource {
29
+ /** Documents directly associated with the issue. */
30
+ issueDocuments(issueId: string): Promise<RawDocument[]>;
31
+ /** The issue's attachment URLs (only the `url` is needed to spot Linear document links). */
32
+ issueAttachmentUrls(issueId: string): Promise<string[]>;
33
+ /** Resolve a Linear document by the slug taken from its URL; null when it does not resolve. */
34
+ documentBySlug(slug: string): Promise<RawDocument | null>;
35
+ }
36
+
37
+ /** The Linear-document slug from an attachment URL, or null when the URL is not a Linear document
38
+ * link. Linear document URLs look like `https://linear.app/<ws>/document/<title-slug>-<slugId>`;
39
+ * the canonical slug Linear's `document(id:)` accepts is the trailing token (the slugId), which is
40
+ * the substring after the final `-`. Falls back to the whole segment when there is no `-`. */
41
+ export function documentSlugFromUrl(url: string): string | null {
42
+ const m = /\/document\/([^/?#]+)/.exec(url);
43
+ if (!m?.[1]) return null;
44
+ // The URL is attacker-influenceable (anyone who can attach a link to the issue). A malformed
45
+ // percent-escape makes decodeURIComponent throw; swallow it and skip this link rather than let it
46
+ // abort the whole fetch (which would discard the issue's other documents too).
47
+ let segment: string;
48
+ try {
49
+ segment = decodeURIComponent(m[1]);
50
+ } catch {
51
+ return null;
52
+ }
53
+ const dash = segment.lastIndexOf("-");
54
+ return dash >= 0 && dash < segment.length - 1 ? segment.slice(dash + 1) : segment;
55
+ }
56
+
57
+ /** A filesystem- and URL-safe slug for the scratch filename: prefer Linear's slugId, else slugify the
58
+ * title, else the id. Bounded length so a long title cannot produce an unwieldy path. */
59
+ export function documentSlug(doc: RawDocument): string {
60
+ if (doc.slugId && doc.slugId.trim()) return doc.slugId.trim();
61
+ const fromTitle = (doc.title ?? "")
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, "-")
64
+ .replace(/^-+|-+$/g, "")
65
+ .slice(0, 60);
66
+ return fromTitle || doc.id;
67
+ }
68
+
69
+ /** Normalise a RawDocument into the contract shape, dropping ones with no usable content. */
70
+ function toAttached(doc: RawDocument): AttachedDocument | null {
71
+ const content = doc.content ?? "";
72
+ if (!content.trim()) return null;
73
+ return {
74
+ id: doc.id,
75
+ slug: documentSlug(doc),
76
+ title: (doc.title ?? "").trim() || "Untitled document",
77
+ url: doc.url ?? "",
78
+ content,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Collect the issue's attached documents from a source, de-duped by id (issue documents first, then
84
+ * attachment-linked documents). Pure of any network: the source does the I/O. Best-effort - an
85
+ * attachment whose document does not resolve is skipped, never fatal.
86
+ */
87
+ export async function collectAttachedDocuments(
88
+ source: DocumentSource,
89
+ issueId: string,
90
+ ): Promise<AttachedDocument[]> {
91
+ const out: AttachedDocument[] = [];
92
+ const seen = new Set<string>();
93
+ const add = (doc: RawDocument | null): void => {
94
+ if (!doc || seen.has(doc.id)) return;
95
+ const attached = toAttached(doc);
96
+ if (!attached) return;
97
+ seen.add(doc.id);
98
+ out.push(attached);
99
+ };
100
+
101
+ for (const doc of await source.issueDocuments(issueId)) add(doc);
102
+
103
+ const urls = await source.issueAttachmentUrls(issueId);
104
+ const slugs = [...new Set(urls.map(documentSlugFromUrl).filter((s): s is string => s !== null))];
105
+ for (const slug of slugs) {
106
+ try {
107
+ add(await source.documentBySlug(slug));
108
+ } catch {
109
+ // best-effort: a deleted/unauthorised document link is skipped, not fatal.
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+
115
+ /** The live `DocumentSource`, backed by a Linear bearer token. Uses the typed SDK. */
116
+ export function linearDocumentSource(token: string): DocumentSource {
117
+ const client = new LinearClient({ accessToken: token });
118
+ return {
119
+ async issueDocuments(issueId) {
120
+ // Bounded, unpaginated cap: an issue with more than 50 attached documents (or 100 attachments
121
+ // below) silently drops the overflow. That is a deliberate limit, not a bug; revisit with cursor
122
+ // pagination if real issues approach it.
123
+ const issue = await client.issue(issueId);
124
+ const conn = await issue.documents({ first: 50 });
125
+ return conn.nodes.map((d) => ({
126
+ id: d.id,
127
+ slugId: d.slugId,
128
+ title: d.title,
129
+ url: d.url,
130
+ content: d.content ?? undefined,
131
+ }));
132
+ },
133
+ async issueAttachmentUrls(issueId) {
134
+ const issue = await client.issue(issueId);
135
+ const conn = await issue.attachments({ first: 100 });
136
+ return conn.nodes.map((a) => a.url).filter((u) => u.length > 0);
137
+ },
138
+ async documentBySlug(slug) {
139
+ const doc = await client.document(slug);
140
+ return {
141
+ id: doc.id,
142
+ slugId: doc.slugId,
143
+ title: doc.title,
144
+ url: doc.url,
145
+ content: doc.content ?? undefined,
146
+ };
147
+ },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Fetch the documents attached to a Linear issue as contract `AttachedDocument`s, ready to snapshot
153
+ * into a run. The single entry point the hub calls; resolves the source from the token and runs the
154
+ * pure assembly. Never throws on a single bad document link (best-effort), but a hard failure (auth,
155
+ * network) propagates so the caller can log and proceed with none.
156
+ */
157
+ export async function fetchAttachedDocuments(
158
+ token: string,
159
+ issueId: string,
160
+ ): Promise<AttachedDocument[]> {
161
+ return collectAttachedDocuments(linearDocumentSource(token), issueId);
162
+ }