@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
package/README.md CHANGED
@@ -1,19 +1,31 @@
1
1
  # @dahrk/linear
2
2
 
3
- **Confidentiality:** Internal
4
- **Status:** DRAFT - UNREVIEWED
3
+ The Linear-native control surface used by [Dahrk](https://dahrk.ai), a Linear-native agent-workflow
4
+ harness. Linear is the substrate and the only source; this package is how Dahrk speaks to it.
5
5
 
6
- The Linear-native control surface and webhook verification. Linear is the substrate and the only
7
- source; this package is how Dahrk speaks to it.
6
+ - **Intake-side:** verify webhook signatures and normalise them to an internal `LinearEvent`.
7
+ - **Session-side:** the Agent Session API - activities, the agent-plan checklist, `elicitation` gates,
8
+ the `prompted` drive, the `stop` signal, and `externalUrls`.
8
9
 
9
- - **Intake-side:** verify webhook signatures; normalise to the internal `LinearEvent`.
10
- - **Session-side:** the Agent Session API - activities, the agent-plan checklist, `elicitation`
11
- gates, the `prompted` drive, the `stop` signal, `externalUrls`.
10
+ Used by both the hub (intake plus session) and the edge node (session, while a stage runs). Implemented
11
+ against the Linear API directly, on top of the Linear TypeScript SDK.
12
12
 
13
- Used by both `hub` (intake + session) and `edge` (session, while a stage runs). Re-implemented
14
- against the API directly; cyrus is a reference. Intended runtime dependency: the Linear TypeScript
15
- SDK. See `../../docs/01-build-spec.md` section 14.
13
+ ## Stability
16
14
 
17
- ## Sources
15
+ This package is pre-1.0, so **a minor bump can break**. Note that a caret range on a `0.x` version does
16
+ not cross the minor: `^0.1.0` will not resolve `0.2.0`. Pin deliberately.
18
17
 
19
- - `../../docs/01-build-spec.md`, the design's doc 05 (Linear feature coverage).
18
+ ## Notes for the curious
19
+
20
+ **There is deliberately no `repository` field**, which is why npm shows no repository row. This package
21
+ is published from a private repo, so the accurate link would 404 for everyone who can read the package,
22
+ and pointing it at the public `dahrk-node` would send readers to a repo the source is not in. The links
23
+ below are the real front doors.
24
+
25
+ ## Links
26
+
27
+ - Documentation: [dahrk.ai/docs](https://dahrk.ai/docs)
28
+ - Issues: [github.com/dahrkai/dahrk-node/issues](https://github.com/dahrkai/dahrk-node/issues)
29
+ - Edge client: [github.com/dahrkai/dahrk-node](https://github.com/dahrkai/dahrk-node)
30
+
31
+ Apache-2.0, © Skakel Labs.
@@ -0,0 +1,36 @@
1
+ import type { IssueComment } from "@dahrk/contracts";
2
+ /** A comment as returned by Linear before normalisation. */
3
+ export interface RawComment {
4
+ id: string;
5
+ body?: string;
6
+ createdAt?: Date | string;
7
+ /** Author user id, compared against the app user id to drop the harness's own posts. */
8
+ authorId?: string;
9
+ /** Author display name, surfaced to the agent. */
10
+ authorName?: string;
11
+ }
12
+ /** The read seam the assembly depends on; the live impl wraps GraphQL, tests inject a fake. */
13
+ export interface CommentSource {
14
+ /** The issue's comments, in whatever order Linear returns them. */
15
+ issueComments(issueId: string): Promise<RawComment[]>;
16
+ /** The id of the app user this token authenticates as, or null when it cannot be resolved. */
17
+ appUserId(): Promise<string | null>;
18
+ }
19
+ /**
20
+ * Collect an issue's comments from a source: drop the app user's own posts, drop empty bodies, and
21
+ * order oldest first so the thread reads as a conversation. Pure of any network - the source does the
22
+ * I/O.
23
+ *
24
+ * Comments with no resolvable timestamp sort last rather than being dropped: an unorderable comment is
25
+ * still content worth showing, and losing it silently would be worse than showing it out of order.
26
+ */
27
+ export declare function collectIssueComments(source: CommentSource, issueId: string): Promise<IssueComment[]>;
28
+ /** The live `CommentSource`, backed by a Linear bearer token. Uses the typed SDK. */
29
+ export declare function linearCommentSource(token: string): CommentSource;
30
+ /**
31
+ * Fetch an issue's comment thread as contract `IssueComment`s, ready to snapshot into a run. The
32
+ * single entry point the hub calls. A hard failure (auth, network) propagates so the caller can log
33
+ * and proceed with none.
34
+ */
35
+ export declare function fetchIssueComments(token: string, issueId: string): Promise<IssueComment[]>;
36
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC1B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+FAA+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACtD,8FAA8F;IAC9F,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACrC;AAYD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,EAAE,CAAC,CAmBzB;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAqChE;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAEhG"}
@@ -0,0 +1,104 @@
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
+ /** Normalise a timestamp Linear may hand back as a Date or a string into ISO 8601. */
17
+ function isoOf(value) {
18
+ if (value instanceof Date)
19
+ return value.toISOString();
20
+ if (typeof value === "string" && value.trim()) {
21
+ const parsed = new Date(value);
22
+ return Number.isNaN(parsed.getTime()) ? "" : parsed.toISOString();
23
+ }
24
+ return "";
25
+ }
26
+ /**
27
+ * Collect an issue's comments from a source: drop the app user's own posts, drop empty bodies, and
28
+ * order oldest first so the thread reads as a conversation. Pure of any network - the source does the
29
+ * I/O.
30
+ *
31
+ * Comments with no resolvable timestamp sort last rather than being dropped: an unorderable comment is
32
+ * still content worth showing, and losing it silently would be worse than showing it out of order.
33
+ */
34
+ export async function collectIssueComments(source, issueId) {
35
+ const [raw, appUserId] = await Promise.all([source.issueComments(issueId), source.appUserId()]);
36
+ const out = [];
37
+ for (const comment of raw) {
38
+ if (appUserId && comment.authorId === appUserId)
39
+ continue;
40
+ const body = (comment.body ?? "").trim();
41
+ if (!body)
42
+ continue;
43
+ out.push({
44
+ id: comment.id,
45
+ author: (comment.authorName ?? "").trim(),
46
+ createdAt: isoOf(comment.createdAt),
47
+ body,
48
+ });
49
+ }
50
+ return out.sort((a, b) => {
51
+ if (!a.createdAt)
52
+ return 1;
53
+ if (!b.createdAt)
54
+ return -1;
55
+ return a.createdAt.localeCompare(b.createdAt);
56
+ });
57
+ }
58
+ /** The live `CommentSource`, backed by a Linear bearer token. Uses the typed SDK. */
59
+ export function linearCommentSource(token) {
60
+ const client = new LinearClient({ accessToken: token });
61
+ return {
62
+ async issueComments(issueId) {
63
+ // Bounded, unpaginated cap, matching `linearDocumentSource.issueDocuments`: a thread longer than
64
+ // 100 comments silently drops the overflow. Deliberate, not a bug - the prompt inlines only the
65
+ // most recent few thousand characters anyway. Revisit with cursor pagination if it bites.
66
+ const issue = await client.issue(issueId);
67
+ const conn = await issue.comments({ first: 100 });
68
+ const nodes = await Promise.all(conn.nodes.map(async (c) => {
69
+ // `user` is a lazy reference on the SDK's Comment; resolving it per comment is what lets us
70
+ // both filter the app user's posts and name the human author. A comment whose author does not
71
+ // resolve (a deleted user, an integration post) still rides through, unattributed.
72
+ const user = await c.user;
73
+ return {
74
+ id: c.id,
75
+ body: c.body,
76
+ createdAt: c.createdAt,
77
+ ...(user?.id ? { authorId: user.id } : {}),
78
+ ...(user?.displayName || user?.name ? { authorName: user.displayName ?? user.name } : {}),
79
+ };
80
+ }));
81
+ return nodes;
82
+ },
83
+ async appUserId() {
84
+ try {
85
+ const viewer = await client.viewer;
86
+ return viewer?.id ?? null;
87
+ }
88
+ catch {
89
+ // Failing to resolve the app user must not lose the thread. We return null and accept that the
90
+ // harness's own comments ride through this once, rather than dropping every comment.
91
+ return null;
92
+ }
93
+ },
94
+ };
95
+ }
96
+ /**
97
+ * Fetch an issue's comment thread as contract `IssueComment`s, ready to snapshot into a run. The
98
+ * single entry point the hub calls. A hard failure (auth, network) propagates so the caller can log
99
+ * and proceed with none.
100
+ */
101
+ export async function fetchIssueComments(token, issueId) {
102
+ return collectIssueComments(linearCommentSource(token), issueId);
103
+ }
104
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.js","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"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"}
@@ -1,17 +1,3 @@
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
1
  import type { AttachedDocument } from "@dahrk/contracts";
16
2
  /** A document as returned by Linear before slug normalisation (slugId is Linear's URL-safe slug). */
17
3
  export interface RawDocument {
@@ -44,7 +30,7 @@ export declare function documentSlug(doc: RawDocument): string;
44
30
  * attachment whose document does not resolve is skipped, never fatal.
45
31
  */
46
32
  export declare function collectAttachedDocuments(source: DocumentSource, issueId: string): Promise<AttachedDocument[]>;
47
- /** The live `DocumentSource`, backed by a Linear bearer token. Plain fetch, no SDK. */
33
+ /** The live `DocumentSource`, backed by a Linear bearer token. Uses the typed SDK. */
48
34
  export declare function linearDocumentSource(token: string): DocumentSource;
49
35
  /**
50
36
  * Fetch the documents attached to a Linear issue as contract `AttachedDocument`s, ready to snapshot
@@ -1 +1 @@
1
- {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIzD,qGAAqG;AACrG,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACxD,4FAA4F;IAC5F,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,+FAA+F;IAC/F,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC3D;AAED;;;+FAG+F;AAC/F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9D;AAED;0FAC0F;AAC1F,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAQrD;AAeD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuB7B;AAqBD,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAgClE;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,qGAAqG;AACrG,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACxD,4FAA4F;IAC5F,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,+FAA+F;IAC/F,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC3D;AAED;;;+FAG+F;AAC/F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9D;AAED;0FAC0F;AAC1F,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAQrD;AAeD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuB7B;AAED,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAiClE;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}
package/dist/documents.js CHANGED
@@ -1,4 +1,18 @@
1
- const GRAPHQL_URL = "https://api.linear.app/graphql";
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";
2
16
  /** The Linear-document slug from an attachment URL, or null when the URL is not a Linear document
3
17
  * link. Linear document URLs look like `https://linear.app/<ws>/document/<title-slug>-<slugId>`;
4
18
  * the canonical slug Linear's `document(id:)` accepts is the trailing token (the slugId), which is
@@ -76,42 +90,38 @@ export async function collectAttachedDocuments(source, issueId) {
76
90
  }
77
91
  return out;
78
92
  }
79
- /** Run one GraphQL query against Linear with a bearer token; throws on transport or GraphQL errors. */
80
- async function gql(token, query, variables) {
81
- const res = await fetch(GRAPHQL_URL, {
82
- method: "POST",
83
- headers: { "content-type": "application/json", authorization: token },
84
- body: JSON.stringify({ query, variables }),
85
- });
86
- if (!res.ok)
87
- throw new Error(`linear graphql ${res.status}`);
88
- const json = (await res.json().catch(() => ({})));
89
- if (json.errors?.length)
90
- throw new Error(json.errors.map((e) => e.message).join("; "));
91
- if (!json.data)
92
- throw new Error("linear graphql: empty response");
93
- return json.data;
94
- }
95
- const DOC_FIELDS = "id slugId title url content";
96
- /** The live `DocumentSource`, backed by a Linear bearer token. Plain fetch, no SDK. */
93
+ /** The live `DocumentSource`, backed by a Linear bearer token. Uses the typed SDK. */
97
94
  export function linearDocumentSource(token) {
95
+ const client = new LinearClient({ accessToken: token });
98
96
  return {
99
97
  async issueDocuments(issueId) {
100
98
  // Bounded, unpaginated cap: an issue with more than 50 attached documents (or 100 attachments
101
99
  // below) silently drops the overflow. That is a deliberate limit, not a bug; revisit with cursor
102
100
  // pagination if real issues approach it.
103
- const data = await gql(token, `query($id: String!){ issue(id:$id){ documents(first:50){ nodes{ ${DOC_FIELDS} } } } }`, { id: issueId });
104
- return data.issue?.documents?.nodes ?? [];
101
+ const issue = await client.issue(issueId);
102
+ const conn = await issue.documents({ first: 50 });
103
+ return conn.nodes.map((d) => ({
104
+ id: d.id,
105
+ slugId: d.slugId,
106
+ title: d.title,
107
+ url: d.url,
108
+ content: d.content ?? undefined,
109
+ }));
105
110
  },
106
111
  async issueAttachmentUrls(issueId) {
107
- const data = await gql(token, `query($id: String!){ issue(id:$id){ attachments(first:100){ nodes{ url } } } }`, { id: issueId });
108
- return (data.issue?.attachments?.nodes ?? [])
109
- .map((a) => a.url)
110
- .filter((u) => typeof u === "string" && u.length > 0);
112
+ const issue = await client.issue(issueId);
113
+ const conn = await issue.attachments({ first: 100 });
114
+ return conn.nodes.map((a) => a.url).filter((u) => u.length > 0);
111
115
  },
112
116
  async documentBySlug(slug) {
113
- const data = await gql(token, `query($id: String!){ document(id:$id){ ${DOC_FIELDS} } }`, { id: slug });
114
- return data.document ?? null;
117
+ const doc = await client.document(slug);
118
+ return {
119
+ id: doc.id,
120
+ slugId: doc.slugId,
121
+ title: doc.title,
122
+ url: doc.url,
123
+ content: doc.content ?? undefined,
124
+ };
115
125
  },
116
126
  };
117
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"documents.js","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"documents.js","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Format a tool call into Linear's action-activity vocabulary (DHK-382): a human verb in
3
+ * `action` and a clean, humanised input in `parameter`. Linear renders these as
4
+ * "<action> · <parameter>" (e.g. "Ran · grep -rn ..."), so the agent session reads as a
5
+ * sequence of verbs instead of a wall of `ToolName {raw JSON args}`.
6
+ *
7
+ * `inputText` is the edge's bounded preview of the tool input: normally a JSON object string
8
+ * (`clip(JSON.stringify(input))`), but it may be TRUNCATED (over the edge's ~500-char cap) or
9
+ * otherwise malformed. Parsing is defensive: a preview that will not parse never throws and
10
+ * never falls through to raw JSON. Known single-field tools still salvage their primary field
11
+ * from a truncated preview; anything unrecoverable degrades to the bare verb.
12
+ */
13
+ /** A tool call rendered as Linear's `{ action, parameter }` pair. */
14
+ export interface ToolAction {
15
+ action: string;
16
+ parameter: string;
17
+ }
18
+ /**
19
+ * Map a `(toolName, inputPreview)` pair to a verb plus a clean parameter. Total and defensive:
20
+ * every branch returns a `{ action, parameter }`, and the parameter is only ever a humanised
21
+ * field value (never the raw JSON), so a malformed or oversized preview degrades to the bare verb.
22
+ */
23
+ export declare function formatToolAction(tool: string | undefined, inputText: string | undefined): ToolAction;
24
+ export declare function formatToolResult(tool: string | undefined, output: string | undefined): string;
25
+ //# sourceMappingURL=format-action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-action.d.ts","sourceRoot":"","sources":["../src/format-action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AA2FD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAgDpG;AAkGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAmB7F"}
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Format a tool call into Linear's action-activity vocabulary (DHK-382): a human verb in
3
+ * `action` and a clean, humanised input in `parameter`. Linear renders these as
4
+ * "<action> · <parameter>" (e.g. "Ran · grep -rn ..."), so the agent session reads as a
5
+ * sequence of verbs instead of a wall of `ToolName {raw JSON args}`.
6
+ *
7
+ * `inputText` is the edge's bounded preview of the tool input: normally a JSON object string
8
+ * (`clip(JSON.stringify(input))`), but it may be TRUNCATED (over the edge's ~500-char cap) or
9
+ * otherwise malformed. Parsing is defensive: a preview that will not parse never throws and
10
+ * never falls through to raw JSON. Known single-field tools still salvage their primary field
11
+ * from a truncated preview; anything unrecoverable degrades to the bare verb.
12
+ */
13
+ /** Keep a parameter readable on one line; the edge already caps the input at ~500 chars. */
14
+ const MAX_PARAM = 400;
15
+ /** Collapse whitespace to a single line and clip with an ellipsis so the parameter never wraps. */
16
+ function clip(s, max = MAX_PARAM) {
17
+ const t = s.replace(/\s+/g, " ").trim();
18
+ return t.length <= max ? t : `${t.slice(0, max - 1).trimEnd()}…`;
19
+ }
20
+ /** The final path segment (e.g. "packages/hub/src/config-server.ts" -> "config-server.ts"). */
21
+ function basename(p) {
22
+ const trimmed = p.replace(/\/+$/, "");
23
+ const i = trimmed.lastIndexOf("/");
24
+ return i >= 0 ? trimmed.slice(i + 1) : trimmed;
25
+ }
26
+ /** Humanise a tool name for display: split separators and camelCase, then Title Case
27
+ * (e.g. "WebFetch" -> "Web Fetch", "create_issue" -> "Create Issue"). */
28
+ function humaniseToolName(name) {
29
+ return (name
30
+ .replace(/[_-]+/g, " ")
31
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
32
+ .trim()
33
+ .replace(/\b\w/g, (c) => c.toUpperCase()) || "Tool");
34
+ }
35
+ /** A parsed tool-input object, or undefined when the preview is missing/truncated/not an object. */
36
+ function parseInput(text) {
37
+ const t = text?.trim();
38
+ if (!t || !t.startsWith("{"))
39
+ return undefined;
40
+ try {
41
+ const parsed = JSON.parse(t);
42
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
43
+ ? parsed
44
+ : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ /** A scalar field as a trimmed string, or undefined when absent/empty/non-scalar. */
51
+ function scalarField(input, key) {
52
+ const v = input?.[key];
53
+ const s = typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : undefined;
54
+ return s && s.trim() !== "" ? s : undefined;
55
+ }
56
+ /** Best-effort extraction of a string field straight from the raw preview, so a TRUNCATED JSON
57
+ * (a long command clipped mid-value) still yields its primary field rather than an empty verb. */
58
+ function looseField(text, key) {
59
+ if (!text)
60
+ return undefined;
61
+ const m = new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)`).exec(text);
62
+ if (!m?.[1])
63
+ return undefined;
64
+ const decoded = m[1]
65
+ .replace(/\\"/g, '"')
66
+ .replace(/\\[nt]/g, " ")
67
+ .replace(/\\\\/g, "\\")
68
+ .replace(/\\$/, "");
69
+ return decoded.trim() !== "" ? decoded : undefined;
70
+ }
71
+ /** A known string field, preferring the parsed value and salvaging a truncated one from the raw text. */
72
+ function stringField(input, text, key) {
73
+ return scalarField(input, key) ?? looseField(text, key);
74
+ }
75
+ /** The first scalar argument of an unknown tool's input, for a best-guess parameter. */
76
+ function firstScalar(input) {
77
+ for (const key of Object.keys(input ?? {})) {
78
+ const s = scalarField(input, key);
79
+ if (s !== undefined)
80
+ return s;
81
+ }
82
+ return "";
83
+ }
84
+ /** A `Read`/inspection line-range suffix (":440-520" or ":440") from numeric offset/limit. */
85
+ function lineRange(input) {
86
+ const offset = input?.offset;
87
+ const limit = input?.limit;
88
+ if (typeof offset !== "number")
89
+ return "";
90
+ return typeof limit === "number" ? `:${offset}-${offset + limit}` : `:${offset}`;
91
+ }
92
+ /**
93
+ * Map a `(toolName, inputPreview)` pair to a verb plus a clean parameter. Total and defensive:
94
+ * every branch returns a `{ action, parameter }`, and the parameter is only ever a humanised
95
+ * field value (never the raw JSON), so a malformed or oversized preview degrades to the bare verb.
96
+ */
97
+ export function formatToolAction(tool, inputText) {
98
+ const name = (tool ?? "").trim();
99
+ const input = parseInput(inputText);
100
+ const field = (key) => stringField(input, inputText, key);
101
+ switch (name) {
102
+ case "Bash":
103
+ return { action: "Ran", parameter: clip(field("command") ?? "") };
104
+ case "Read": {
105
+ const path = field("file_path");
106
+ return { action: "Read", parameter: path ? clip(basename(path) + lineRange(input)) : "" };
107
+ }
108
+ case "Grep": {
109
+ const pattern = field("pattern");
110
+ const path = field("path");
111
+ const where = pattern ? `"${pattern}"${path ? ` in ${path}` : ""}` : "";
112
+ return { action: "Searched", parameter: clip(where) };
113
+ }
114
+ case "Glob": {
115
+ const pattern = field("pattern");
116
+ const path = field("path");
117
+ const where = pattern ? `${pattern}${path ? ` in ${path}` : ""}` : "";
118
+ return { action: "Searched", parameter: clip(where) };
119
+ }
120
+ case "Edit":
121
+ case "MultiEdit": {
122
+ const path = field("file_path");
123
+ return { action: "Edited", parameter: path ? clip(basename(path)) : "" };
124
+ }
125
+ case "Write": {
126
+ const path = field("file_path");
127
+ return { action: "Wrote", parameter: path ? clip(basename(path)) : "" };
128
+ }
129
+ case "ToolSearch":
130
+ return { action: "Loaded tools", parameter: clip(field("query") ?? "") };
131
+ default:
132
+ break;
133
+ }
134
+ // MCP tools arrive as `mcp__<server>__<tool>`; show the humanised tool segment.
135
+ if (name.startsWith("mcp__")) {
136
+ const segments = name.split("__").filter(Boolean);
137
+ const toolSegment = segments[segments.length - 1] ?? name;
138
+ return { action: humaniseToolName(toolSegment), parameter: clip(firstScalar(input)) };
139
+ }
140
+ // Unknown tool: a title-cased name plus its first scalar argument.
141
+ return { action: humaniseToolName(name), parameter: clip(firstScalar(input)) };
142
+ }
143
+ /**
144
+ * Render a completed tool's output as the markdown `result` of its action activity (DHK-386). This
145
+ * is the companion to formatToolAction: the verb + parameter describe the call, this shapes the
146
+ * outcome per tool - `Bash` as a fenced block, `Grep` as a match-count summary, `Edit`/`Write` as a
147
+ * diff stat, `Read` as how much was read - so the session reads richly instead of dumping raw text.
148
+ *
149
+ * `output` is the tool's full (untruncated) observation output. Total and defensive like its
150
+ * companion: it never throws, bounds every long output with a clear elision so a noisy tool can
151
+ * never flood the session, and degrades any unrecognised tool or shape to a bounded first line. An
152
+ * empty output yields an empty string, so no `result` is folded onto the action.
153
+ */
154
+ /** Caps for a rendered result: keep the outcome glanceable. Long output shows a head and tail with
155
+ * a "… (N more lines)" marker between, never the whole wall. */
156
+ const RESULT_MAX_LINES = 12;
157
+ const RESULT_HEAD_LINES = 8;
158
+ const RESULT_TAIL_LINES = 3;
159
+ const RESULT_MAX_CHARS = 1800;
160
+ const RESULT_TOP_HITS = 5;
161
+ /** Clip a multi-line block to a character budget, marking the cut with an ellipsis line. */
162
+ function clipBlock(s, max = RESULT_MAX_CHARS) {
163
+ return s.length <= max ? s : `${s.slice(0, max).trimEnd()}\n…`;
164
+ }
165
+ /** Bound a block of lines: keep it whole when short, else show the head and tail with a
166
+ * "… (N more lines)" elision marker between them so long output never becomes a wall of text. */
167
+ function elideLines(lines) {
168
+ if (lines.length <= RESULT_MAX_LINES)
169
+ return lines.join("\n");
170
+ const head = lines.slice(0, RESULT_HEAD_LINES);
171
+ const tail = lines.slice(-RESULT_TAIL_LINES);
172
+ const omitted = lines.length - head.length - tail.length;
173
+ return [...head, `… (${omitted} more lines)`, ...tail].join("\n");
174
+ }
175
+ /** A fenced code block, the standard rendering for verbatim command/file output. */
176
+ function codeFence(body) {
177
+ return "```\n" + body + "\n```";
178
+ }
179
+ /** The first non-empty line, collapsed and clipped - the safe fallback for MCP and unknown tools. */
180
+ function firstLine(output) {
181
+ return clip(output.split("\n").find((l) => l.trim() !== "") ?? "");
182
+ }
183
+ /** English pluralisation for a count, e.g. `plural(2, "match", "matches")`. */
184
+ function plural(n, one, many) {
185
+ return `${n} ${n === 1 ? one : many}`;
186
+ }
187
+ /** `Bash`: the stdout/stderr in a fenced code block, bounded head + tail. */
188
+ function renderBashResult(output) {
189
+ return codeFence(clipBlock(elideLines(output.split("\n"))));
190
+ }
191
+ /** A `path:line` grep hit parsed from a `path:line:content` or `path:line` output row. */
192
+ function grepHit(row) {
193
+ const m = /^(.+?):(\d+)(?::|$)/.exec(row);
194
+ return m ? { path: m[1], line: m[2] } : undefined;
195
+ }
196
+ /** `Grep`: an "N matches in M files" summary plus the first few ``path:line`` hits. Falls back to a
197
+ * plain match count and the first rows when the output is not the `path:line` shape (e.g. a
198
+ * files-with-matches listing of bare paths). */
199
+ function renderGrepResult(output) {
200
+ const rows = output.split("\n").map((r) => r.trim()).filter(Boolean);
201
+ const hits = rows.map(grepHit).filter((h) => h !== undefined);
202
+ if (hits.length === 0) {
203
+ const listed = rows.slice(0, RESULT_TOP_HITS).map((r) => `- ${r}`);
204
+ const more = rows.length > RESULT_TOP_HITS ? [`… (${rows.length - RESULT_TOP_HITS} more)`] : [];
205
+ return [plural(rows.length, "match", "matches"), ...listed, ...more].join("\n");
206
+ }
207
+ const files = new Set(hits.map((h) => h.path)).size;
208
+ const summary = `${plural(hits.length, "match", "matches")} in ${plural(files, "file", "files")}`;
209
+ const top = hits.slice(0, RESULT_TOP_HITS).map((h) => `- \`${h.path}:${h.line}\``);
210
+ const more = hits.length > RESULT_TOP_HITS ? [`… (${hits.length - RESULT_TOP_HITS} more)`] : [];
211
+ return [summary, ...top, ...more].join("\n");
212
+ }
213
+ /** `Read`: how much was read; the file content itself is not worth echoing into the session. */
214
+ function renderReadResult(output) {
215
+ return `Read ${plural(output.split("\n").length, "line", "lines")}.`;
216
+ }
217
+ /** `Edit`/`Write`: a `+added / −removed` diff stat when the output is a unified diff, else the tool's
218
+ * one-line confirmation. Ignores the `+++`/`---` file headers so they are not counted as changes. */
219
+ function renderEditResult(output) {
220
+ let added = 0;
221
+ let removed = 0;
222
+ for (const line of output.split("\n")) {
223
+ if (line.startsWith("+") && !line.startsWith("+++"))
224
+ added += 1;
225
+ else if (line.startsWith("-") && !line.startsWith("---"))
226
+ removed += 1;
227
+ }
228
+ return added > 0 || removed > 0 ? `\`+${added} / −${removed}\`` : firstLine(output);
229
+ }
230
+ export function formatToolResult(tool, output) {
231
+ const text = (output ?? "").trimEnd();
232
+ if (text.trim() === "")
233
+ return "";
234
+ switch ((tool ?? "").trim()) {
235
+ case "Bash":
236
+ return renderBashResult(text);
237
+ case "Grep":
238
+ return renderGrepResult(text);
239
+ case "Read":
240
+ return renderReadResult(text);
241
+ case "Edit":
242
+ case "MultiEdit":
243
+ case "Write":
244
+ return renderEditResult(text);
245
+ default:
246
+ // MCP and unknown tools: a short, safe summary rather than a raw dump.
247
+ return firstLine(text);
248
+ }
249
+ }
250
+ //# sourceMappingURL=format-action.js.map