toolcraft 0.0.22 → 0.0.24

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 (147) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +57 -14
  5. package/dist/error-report.js +32 -3
  6. package/dist/human-in-loop/approval-tasks.d.ts +1 -0
  7. package/dist/human-in-loop/approval-tasks.js +7 -5
  8. package/dist/human-in-loop/approvals-commands.js +51 -8
  9. package/dist/human-in-loop/runner.js +24 -19
  10. package/dist/human-in-loop/state-machine.d.ts +3 -3
  11. package/dist/human-in-loop/state-machine.js +13 -5
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -1
  14. package/dist/mcp-proxy.js +85 -19
  15. package/dist/mcp.compile-check.js +1 -0
  16. package/dist/mcp.d.ts +1 -0
  17. package/dist/mcp.js +50 -8
  18. package/dist/renderer.js +119 -13
  19. package/dist/sdk.compile-check.js +1 -0
  20. package/dist/sdk.d.ts +1 -0
  21. package/dist/sdk.js +56 -11
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
  24. package/node_modules/@poe-code/agent-defs/package.json +1 -1
  25. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
  26. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
  27. package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
  28. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
  29. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
  30. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
  31. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
  32. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
  33. package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
  34. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
  35. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
  38. package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
  42. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
  43. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
  44. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
  45. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
  46. package/node_modules/@poe-code/config-mutations/package.json +1 -1
  47. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  48. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  49. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  50. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  51. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  52. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  53. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  54. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  55. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  56. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  57. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  58. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  59. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  60. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  61. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  62. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  63. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  64. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  65. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  66. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  67. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  68. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  73. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  75. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  76. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  80. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  81. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  82. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  83. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  84. package/node_modules/@poe-code/design-system/package.json +2 -1
  85. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +244 -110
  86. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +16 -4
  87. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  88. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +16 -1
  89. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  90. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  91. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  92. package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
  93. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +57 -0
  94. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +484 -0
  95. package/node_modules/@poe-code/process-runner/package.json +1 -1
  96. package/node_modules/@poe-code/task-list/README.md +0 -2
  97. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  98. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  99. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  100. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  101. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  102. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  103. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  104. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  105. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  106. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  107. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  108. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  109. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  110. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  111. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  112. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  113. package/node_modules/@poe-code/task-list/package.json +1 -2
  114. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  115. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +7 -0
  116. package/node_modules/auth-store/dist/encrypted-file-store.js +69 -7
  117. package/node_modules/auth-store/dist/index.d.ts +1 -1
  118. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  119. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  120. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  121. package/node_modules/auth-store/dist/provider-store.js +55 -7
  122. package/node_modules/auth-store/dist/types.d.ts +3 -1
  123. package/node_modules/auth-store/package.json +2 -1
  124. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  125. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  126. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  127. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  128. package/node_modules/mcp-oauth/package.json +1 -0
  129. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  130. package/node_modules/tiny-mcp-client/dist/internal.d.ts +8 -4
  131. package/node_modules/tiny-mcp-client/dist/internal.js +237 -67
  132. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  133. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  134. package/node_modules/tiny-mcp-client/package.json +2 -1
  135. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  136. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  137. package/node_modules/tiny-mcp-client/src/internal.ts +279 -77
  138. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  139. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  140. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  141. package/package.json +10 -12
  142. package/node_modules/@poe-code/file-lock/README.md +0 -52
  143. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  144. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  145. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  146. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  147. package/node_modules/@poe-code/file-lock/package.json +0 -23
@@ -11,7 +11,7 @@ export const PROJECT_ORGANIZATION_QUERY = `query Project($owner: String!, $numbe
11
11
  ... on ProjectV2SingleSelectField {
12
12
  id
13
13
  name
14
- options { id name }
14
+ options { id name color description }
15
15
  }
16
16
  }
17
17
  fields(first: 100) {
@@ -19,7 +19,7 @@ export const PROJECT_ORGANIZATION_QUERY = `query Project($owner: String!, $numbe
19
19
  ... on ProjectV2SingleSelectField {
20
20
  id
21
21
  name
22
- options { id name }
22
+ options { id name color description }
23
23
  }
24
24
  }
25
25
  }
@@ -35,7 +35,7 @@ export const PROJECT_USER_QUERY = `query Project($owner: String!, $number: Int!)
35
35
  ... on ProjectV2SingleSelectField {
36
36
  id
37
37
  name
38
- options { id name }
38
+ options { id name color description }
39
39
  }
40
40
  }
41
41
  fields(first: 100) {
@@ -43,7 +43,7 @@ export const PROJECT_USER_QUERY = `query Project($owner: String!, $number: Int!)
43
43
  ... on ProjectV2SingleSelectField {
44
44
  id
45
45
  name
46
- options { id name }
46
+ options { id name color description }
47
47
  }
48
48
  }
49
49
  }
@@ -80,9 +80,28 @@ const PROJECT_ITEMS_QUERY = `query Items($projectId: ID!, $after: String) {
80
80
  }
81
81
  }
82
82
  }`;
83
- const ISSUE_QUERY = `query Issue($owner: String!, $repo: String!, $number: Int!) {
83
+ const REPOSITORY_ISSUES_QUERY = `query Issues($owner: String!, $repo: String!, $labels: [String!], $after: String) {
84
+ repository(owner: $owner, name: $repo) {
85
+ issues(first: 100, after: $after, labels: $labels, states: OPEN) {
86
+ nodes {
87
+ __typename
88
+ number
89
+ title
90
+ body
91
+ url
92
+ createdAt
93
+ labels(first: 50) { nodes { name } }
94
+ assignees(first: 20) { nodes { login } }
95
+ milestone { title }
96
+ }
97
+ pageInfo { hasNextPage endCursor }
98
+ }
99
+ }
100
+ }`;
101
+ const ISSUE_QUERY = `query Issue($owner: String!, $repo: String!, $number: Int!, $after: String) {
84
102
  repository(owner: $owner, name: $repo) {
85
103
  issue(number: $number) {
104
+ id
86
105
  number
87
106
  title
88
107
  body
@@ -91,7 +110,7 @@ const ISSUE_QUERY = `query Issue($owner: String!, $repo: String!, $number: Int!)
91
110
  labels(first: 50) { nodes { name } }
92
111
  assignees(first: 20) { nodes { login } }
93
112
  milestone { title }
94
- projectItems(first: 10) {
113
+ projectItems(first: 100, after: $after) {
95
114
  nodes {
96
115
  id
97
116
  project { id }
@@ -101,40 +120,80 @@ const ISSUE_QUERY = `query Issue($owner: String!, $repo: String!, $number: Int!)
101
120
  }
102
121
  }
103
122
  }
123
+ pageInfo { hasNextPage endCursor }
104
124
  }
105
125
  }
106
126
  }
107
127
  }`;
108
- const REPOSITORY_QUERY = `query Repository($owner: String!, $repo: String!) {
109
- repository(owner: $owner, name: $repo) {
110
- id
111
- }
112
- }`;
113
- const ISSUE_ID_QUERY = `query IssueId($owner: String!, $repo: String!, $number: Int!) {
128
+ const REPOSITORY_ISSUE_QUERY = `query Issue($owner: String!, $repo: String!, $number: Int!) {
114
129
  repository(owner: $owner, name: $repo) {
115
130
  issue(number: $number) {
116
131
  id
132
+ number
133
+ title
134
+ body
135
+ url
136
+ createdAt
137
+ labels(first: 50) { nodes { name } }
138
+ assignees(first: 20) { nodes { login } }
139
+ milestone { title }
117
140
  }
118
141
  }
119
142
  }`;
120
- const ISSUE_PROJECT_ITEM_QUERY = `query IssueProjectItem($owner: String!, $repo: String!, $number: Int!) {
143
+ const ISSUE_STATE_LABELS_QUERY = `query IssueStateLabels($owner: String!, $repo: String!, $number: Int!, $after: String) {
121
144
  repository(owner: $owner, name: $repo) {
122
145
  issue(number: $number) {
123
146
  id
124
- projectItems(first: 10) {
147
+ labels(first: 50) { nodes { id name } }
148
+ projectItems(first: 100, after: $after) {
125
149
  nodes {
126
150
  id
127
151
  project { id }
128
152
  }
153
+ pageInfo { hasNextPage endCursor }
129
154
  }
130
155
  }
131
156
  }
132
157
  }`;
158
+ const REPOSITORY_ISSUE_STATE_LABELS_QUERY = `query IssueStateLabels($owner: String!, $repo: String!, $number: Int!) {
159
+ repository(owner: $owner, name: $repo) {
160
+ issue(number: $number) {
161
+ id
162
+ labels(first: 50) { nodes { id name } }
163
+ }
164
+ }
165
+ }`;
166
+ const REPOSITORY_QUERY = `query Repository($owner: String!, $repo: String!) {
167
+ repository(owner: $owner, name: $repo) {
168
+ id
169
+ }
170
+ }`;
171
+ const ISSUE_ID_QUERY = `query IssueId($owner: String!, $repo: String!, $number: Int!) {
172
+ repository(owner: $owner, name: $repo) {
173
+ issue(number: $number) {
174
+ id
175
+ }
176
+ }
177
+ }`;
178
+ const REPOSITORY_LABEL_QUERY = `query RepositoryLabel($owner: String!, $repo: String!, $name: String!) {
179
+ repository(owner: $owner, name: $repo) {
180
+ label(name: $name) {
181
+ id
182
+ }
183
+ }
184
+ }`;
133
185
  const CREATE_ISSUE_MUTATION = `mutation CreateIssue($input: CreateIssueInput!) {
134
186
  createIssue(input: $input) {
135
187
  issue {
136
188
  id
137
189
  number
190
+ title
191
+ body
192
+ url
193
+ createdAt
194
+ labels(first: 50) { nodes { name } }
195
+ assignees(first: 20) { nodes { login } }
196
+ milestone { title }
138
197
  }
139
198
  }
140
199
  }`;
@@ -159,6 +218,16 @@ const UPDATE_ISSUE_MUTATION = `mutation UpdateIssue($input: UpdateIssueInput!) {
159
218
  }
160
219
  }
161
220
  }`;
221
+ const ADD_LABELS_MUTATION = `mutation AddLabels($input: AddLabelsToLabelableInput!) {
222
+ addLabelsToLabelable(input: $input) {
223
+ clientMutationId
224
+ }
225
+ }`;
226
+ const REMOVE_LABELS_MUTATION = `mutation RemoveLabels($input: RemoveLabelsFromLabelableInput!) {
227
+ removeLabelsFromLabelable(input: $input) {
228
+ clientMutationId
229
+ }
230
+ }`;
162
231
  const ADD_COMMENT_MUTATION = `mutation AddComment($input: AddCommentInput!) {
163
232
  addComment(input: $input) {
164
233
  commentEdge {
@@ -179,39 +248,52 @@ const DELETE_PROJECT_ITEM_MUTATION = `mutation DeleteProjectItem($input: DeleteP
179
248
  }
180
249
  }`;
181
250
  export async function ghIssuesBackend(deps) {
251
+ if (deps.state?.labelPrefix === "") {
252
+ throw new Error("gh-issues state.labelPrefix must be a non-empty string when configured.");
253
+ }
182
254
  const client = createGhClient({
183
255
  token: deps.token,
184
256
  endpoint: deps.endpoint,
185
257
  fetch: deps.fetch
186
258
  });
187
- const listName = `${deps.project.owner}/${deps.project.number}`;
188
- const variables = {
189
- owner: deps.project.owner,
190
- number: deps.project.number
191
- };
192
- const organizationResult = await client.graphql(PROJECT_ORGANIZATION_QUERY, variables);
193
- let project = organizationResult.organization?.projectV2 ?? null;
194
- if (project === null) {
195
- const userResult = await client.graphql(PROJECT_USER_QUERY, variables);
196
- project = userResult.user?.projectV2 ?? null;
197
- }
198
- if (project === null) {
199
- throw new Error(`Project ${listName} not found or inaccessible.`);
200
- }
201
- const field = project.field;
202
- if (!isStatusField(field)) {
203
- throw new Error(`Project ${listName} has no Status field; gh-issues requires one.`);
259
+ const repoParts = parseRepo(deps.repo);
260
+ const project = deps.project;
261
+ let listName;
262
+ let session;
263
+ if (project === undefined) {
264
+ if (deps.state?.labelPrefix === undefined || deps.stateMachine === undefined) {
265
+ throw new Error("gh-issues requires project or label-backed stateMachine configuration.");
266
+ }
267
+ listName = deps.repo;
268
+ session = createLabelSession(deps.stateMachine, deps.state.labelPrefix);
204
269
  }
205
- if (field.options.length === 0) {
206
- throw new Error(`Project ${listName} Status field has no options.`);
270
+ else {
271
+ listName = `${project.owner}/${project.number}`;
272
+ const variables = { owner: project.owner, number: project.number };
273
+ const organizationResult = await client.graphql(PROJECT_ORGANIZATION_QUERY, variables);
274
+ let resolvedProject = organizationResult.organization?.projectV2 ?? null;
275
+ if (resolvedProject === null) {
276
+ const userResult = await client.graphql(PROJECT_USER_QUERY, variables);
277
+ resolvedProject = userResult.user?.projectV2 ?? null;
278
+ }
279
+ if (resolvedProject === null) {
280
+ throw new Error(`Project ${listName} not found or inaccessible.`);
281
+ }
282
+ const field = resolvedProject.field;
283
+ if (!isStatusField(field)) {
284
+ throw new Error(`Project ${listName} has no Status field; gh-issues requires one.`);
285
+ }
286
+ if (field.options.length === 0) {
287
+ throw new Error(`Project ${listName} Status field has no options.`);
288
+ }
289
+ session = createProjectSession(resolvedProject, field, deps.state?.labelPrefix);
207
290
  }
208
- const session = createSession(project, field);
209
- const repoParts = parseRepo(deps.repo);
210
291
  const context = {
211
292
  client,
212
293
  repoOwner: repoParts.owner,
213
294
  repoName: repoParts.name,
214
- issueIds: new Map()
295
+ issueIds: new Map(),
296
+ labels: resolveLabelsFilter(deps.filter)
215
297
  };
216
298
  function list(name) {
217
299
  assertSingleList(name, listName);
@@ -234,7 +316,7 @@ export async function ghIssuesBackend(deps) {
234
316
  }
235
317
  };
236
318
  }
237
- function createSession(project, field) {
319
+ function createProjectSession(project, field, labelPrefix) {
238
320
  const statusOptions = new Map(field.options.map((option) => [option.name, option.id]));
239
321
  const states = field.options.map((option) => option.name);
240
322
  const events = Object.fromEntries(states.map((state) => [state, Object.freeze({ from: "*", to: state })]));
@@ -247,7 +329,17 @@ function createSession(project, field) {
247
329
  projectId: project.id,
248
330
  statusFieldId: field.id,
249
331
  statusOptions,
250
- stateMachine
332
+ stateMachine,
333
+ labelPrefix,
334
+ labelIds: new Map()
335
+ });
336
+ }
337
+ function createLabelSession(stateMachine, labelPrefix) {
338
+ return Object.freeze({
339
+ statusOptions: new Map(stateMachine.states.map((state) => [state, state])),
340
+ stateMachine,
341
+ labelPrefix,
342
+ labelIds: new Map()
251
343
  });
252
344
  }
253
345
  function createTasksView(name, session, context) {
@@ -258,7 +350,7 @@ function createTasksView(name, session, context) {
258
350
  if (filter?.includeArchived === true) {
259
351
  return [];
260
352
  }
261
- const tasks = await fetchProjectTasks(name, session, context);
353
+ const tasks = await fetchTasks(name, session, context);
262
354
  const filteredTasks = filter?.state === undefined ? tasks : tasks.filter((task) => task.state === filter.state);
263
355
  if (filter?.order === "alphabetical") {
264
356
  return sortTasks(filteredTasks);
@@ -281,34 +373,83 @@ function createTasksView(name, session, context) {
281
373
  */
282
374
  async create(input) {
283
375
  const repositoryId = await resolveRepositoryId(context);
376
+ const labelIds = await resolveConfiguredLabelIds(session, context);
284
377
  const created = await context.client.graphql(CREATE_ISSUE_MUTATION, {
285
378
  input: {
286
379
  repositoryId,
287
380
  title: input.name,
288
- body: input.description ?? ""
381
+ body: input.description ?? "",
382
+ ...(labelIds.length === 0 ? {} : { labelIds })
289
383
  }
290
384
  });
291
385
  const issue = created.createIssue?.issue;
292
386
  const issueId = issue?.id ?? null;
293
387
  const issueNumber = issue?.number ?? null;
294
- if (issueId === null || issueNumber === null) {
388
+ if (issue === undefined || issue === null || issueId === null || issueNumber === null) {
295
389
  throw new Error("GitHub createIssue response did not include issue id and number.");
296
390
  }
297
391
  context.issueIds.set(issueNumber, issueId);
298
- const added = await context.client.graphql(ADD_PROJECT_ITEM_MUTATION, {
299
- input: {
300
- projectId: session.projectId,
301
- contentId: issueId
392
+ let projectItemId;
393
+ try {
394
+ if (session.projectId !== undefined) {
395
+ const added = await context.client.graphql(ADD_PROJECT_ITEM_MUTATION, {
396
+ input: {
397
+ projectId: session.projectId,
398
+ contentId: issueId
399
+ }
400
+ });
401
+ projectItemId = added.addProjectV2ItemById?.item?.id ?? undefined;
402
+ if (projectItemId === undefined) {
403
+ throw new Error("GitHub addProjectV2ItemById response did not include project item id.");
404
+ }
302
405
  }
303
- });
304
- const projectItemId = added.addProjectV2ItemById?.item?.id ?? null;
305
- if (projectItemId === null) {
306
- throw new Error("GitHub addProjectV2ItemById response did not include project item id.");
406
+ if (session.labelPrefix === undefined) {
407
+ if (projectItemId === undefined) {
408
+ throw new Error("gh-issues project-backed state requires a project item id.");
409
+ }
410
+ await updateProjectItemStatus(projectItemId, session.stateMachine.initial, session, context);
411
+ }
412
+ else {
413
+ await addStateLabel(issueId, session.stateMachine.initial, session, context);
414
+ }
415
+ }
416
+ catch (error) {
417
+ if (projectItemId !== undefined && session.projectId !== undefined) {
418
+ await context.client.graphql(DELETE_PROJECT_ITEM_MUTATION, {
419
+ input: { projectId: session.projectId, itemId: projectItemId }
420
+ });
421
+ }
422
+ await context.client.graphql(UPDATE_ISSUE_MUTATION, {
423
+ input: { id: issueId, state: "CLOSED" }
424
+ });
425
+ throw error;
307
426
  }
308
- await updateProjectItemStatus(projectItemId, session.stateMachine.initial, session, context);
309
- return fetchIssueTask(String(issueNumber), name, session, context);
427
+ const task = mapIssueToTask({
428
+ issue: {
429
+ ...issue,
430
+ number: issueNumber,
431
+ title: input.name,
432
+ body: input.description ?? "",
433
+ url: issue.url,
434
+ createdAt: issue.createdAt,
435
+ labels: {
436
+ nodes: [
437
+ ...(context.labels ?? []).map((label) => ({ name: label })),
438
+ ...(session.labelPrefix === undefined
439
+ ? []
440
+ : [{ name: `${session.labelPrefix}${session.stateMachine.initial}` }])
441
+ ]
442
+ }
443
+ },
444
+ projectItemId,
445
+ statusName: session.stateMachine.initial,
446
+ listName: name,
447
+ session
448
+ });
449
+ return session.labelPrefix === undefined ? task : { ...task, state: session.stateMachine.initial };
310
450
  },
311
451
  async update(id, patch) {
452
+ const task = await fetchIssueTask(id, name, session, context);
312
453
  const input = {};
313
454
  if (patch.name !== undefined) {
314
455
  input.title = patch.name;
@@ -323,7 +464,11 @@ function createTasksView(name, session, context) {
323
464
  input
324
465
  });
325
466
  }
326
- return fetchIssueTask(id, name, session, context);
467
+ return {
468
+ ...task,
469
+ name: patch.name ?? task.name,
470
+ description: patch.description ?? task.description
471
+ };
327
472
  },
328
473
  async fire(id, event, _opts) {
329
474
  if (!session.statusOptions.has(event)) {
@@ -333,12 +478,28 @@ function createTasksView(name, session, context) {
333
478
  reason: `Unknown gh-issues Status state "${event}".`
334
479
  });
335
480
  }
336
- const projectItemId = await resolveProjectItemId(id, name, session, context);
481
+ const task = await fetchIssueTask(id, name, session, context);
482
+ const transition = findEvent(session.stateMachine, task.state, event);
483
+ if (transition === undefined) {
484
+ throw new InvalidTransitionError({
485
+ task,
486
+ event,
487
+ to: event,
488
+ reason: `Cannot fire event "${event}" from task state "${task.state}".`
489
+ });
490
+ }
337
491
  // opts.metadataPatch writes are out of scope for v1 on gh-issues.
338
- await updateProjectItemStatus(projectItemId, event, session, context);
339
- return fetchIssueTask(id, name, session, context);
492
+ if (session.labelPrefix === undefined) {
493
+ const projectItemId = projectItemIdFromTask(task);
494
+ await updateProjectItemStatus(projectItemId, event, session, context);
495
+ }
496
+ else {
497
+ await updateIssueStateLabel(id, name, event, session, context);
498
+ }
499
+ return { ...task, state: event };
340
500
  },
341
501
  async comment(id, body) {
502
+ await fetchIssueTask(id, name, session, context);
342
503
  await context.client.graphql(ADD_COMMENT_MUTATION, {
343
504
  input: {
344
505
  subjectId: await resolveIssueId(id, name, context),
@@ -347,12 +508,23 @@ function createTasksView(name, session, context) {
347
508
  });
348
509
  },
349
510
  async canFire(id, event) {
350
- return findEvent(session.stateMachine, id, event) !== undefined;
511
+ const task = await fetchIssueTask(id, name, session, context);
512
+ return findEvent(session.stateMachine, task.state, event) !== undefined;
351
513
  },
352
514
  async events(id) {
353
- return eventsFromState(session.stateMachine, id);
515
+ const task = await fetchIssueTask(id, name, session, context);
516
+ return eventsFromState(session.stateMachine, task.state);
354
517
  },
355
518
  async delete(id) {
519
+ if (session.projectId === undefined) {
520
+ await context.client.graphql(UPDATE_ISSUE_MUTATION, {
521
+ input: {
522
+ id: await resolveIssueId(id, name, context),
523
+ state: "CLOSED"
524
+ }
525
+ });
526
+ return;
527
+ }
356
528
  const projectItemId = await resolveProjectItemId(id, name, session, context);
357
529
  await context.client.graphql(DELETE_PROJECT_ITEM_MUTATION, {
358
530
  input: {
@@ -362,12 +534,15 @@ function createTasksView(name, session, context) {
362
534
  });
363
535
  },
364
536
  async move(id, anchor) {
365
- const projectItemId = await resolveProjectItemId(id, name, session, context);
537
+ assertProjectBacked(session, "move");
538
+ const task = await fetchIssueTask(id, name, session, context);
539
+ const projectItemId = projectItemIdFromTask(task);
366
540
  const afterId = await resolveMoveAfterId(id, anchor, name, session, context);
367
541
  await updateProjectItemPosition(projectItemId, afterId, session, context);
368
- return fetchIssueTask(id, name, session, context);
542
+ return task;
369
543
  },
370
544
  async reorder(ids) {
545
+ assertProjectBacked(session, "reorder");
371
546
  const currentTasks = await fetchProjectTasks(name, session, context);
372
547
  const currentIds = currentTasks.map((task) => task.id);
373
548
  const currentSet = new Set(currentIds);
@@ -389,18 +564,32 @@ function createTasksView(name, session, context) {
389
564
  }
390
565
  const itemIdsByTaskId = new Map(currentTasks.map((task) => [task.id, projectItemIdFromTask(task)]));
391
566
  let afterId = null;
392
- for (const id of ids) {
393
- const projectItemId = itemIdsByTaskId.get(id);
394
- if (projectItemId === undefined) {
395
- throw new OrderMismatchError({ missing: [id], extra: [] });
567
+ try {
568
+ for (const id of ids) {
569
+ const projectItemId = itemIdsByTaskId.get(id);
570
+ if (projectItemId === undefined) {
571
+ throw new OrderMismatchError({ missing: [id], extra: [] });
572
+ }
573
+ await updateProjectItemPosition(projectItemId, afterId, session, context);
574
+ afterId = projectItemId;
396
575
  }
397
- await updateProjectItemPosition(projectItemId, afterId, session, context);
398
- afterId = projectItemId;
576
+ }
577
+ catch (error) {
578
+ await restoreProjectOrder(currentTasks, session, context);
579
+ throw error;
399
580
  }
400
581
  return fetchProjectTasks(name, session, context);
401
582
  }
402
583
  };
403
584
  }
585
+ async function restoreProjectOrder(tasks, session, context) {
586
+ let afterId = null;
587
+ for (const task of tasks) {
588
+ const projectItemId = projectItemIdFromTask(task);
589
+ await updateProjectItemPosition(projectItemId, afterId, session, context);
590
+ afterId = projectItemId;
591
+ }
592
+ }
404
593
  async function resolveRepositoryId(context) {
405
594
  if (context.repositoryId !== undefined) {
406
595
  return context.repositoryId;
@@ -435,26 +624,36 @@ async function resolveIssueId(id, listName, context) {
435
624
  return issueId;
436
625
  }
437
626
  async function resolveProjectItemId(id, listName, session, context) {
627
+ assertProjectBacked(session, "resolve project item");
438
628
  const issueNumber = parseIssueNumber(id, listName);
439
- const result = await context.client.graphql(ISSUE_PROJECT_ITEM_QUERY, {
440
- owner: context.repoOwner,
441
- repo: context.repoName,
442
- number: issueNumber
443
- });
444
- const issue = result.repository?.issue ?? null;
445
- if (issue === null) {
446
- throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
447
- }
448
- if (issue.id !== undefined && issue.id !== null) {
449
- context.issueIds.set(issueNumber, issue.id);
450
- }
451
- const projectItem = issue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
452
- if (projectItem === null) {
453
- throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
629
+ let after;
630
+ while (true) {
631
+ const result = await context.client.graphql(ISSUE_STATE_LABELS_QUERY, {
632
+ owner: context.repoOwner,
633
+ repo: context.repoName,
634
+ number: issueNumber,
635
+ after
636
+ });
637
+ const issue = result.repository?.issue ?? null;
638
+ if (issue === null) {
639
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
640
+ }
641
+ if (issue.id !== undefined && issue.id !== null) {
642
+ context.issueIds.set(issueNumber, issue.id);
643
+ }
644
+ const projectItem = issue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
645
+ if (projectItem !== null) {
646
+ return projectItem.id;
647
+ }
648
+ const pageInfo = issue.projectItems?.pageInfo;
649
+ if (pageInfo?.hasNextPage !== true || pageInfo.endCursor === undefined || pageInfo.endCursor === null) {
650
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
651
+ }
652
+ after = pageInfo.endCursor;
454
653
  }
455
- return projectItem.id;
456
654
  }
457
655
  async function updateProjectItemStatus(projectItemId, state, session, context) {
656
+ assertProjectBacked(session, "set project status");
458
657
  const optionId = session.statusOptions.get(state);
459
658
  if (optionId === undefined) {
460
659
  throw new InvalidTransitionError({
@@ -473,7 +672,103 @@ async function updateProjectItemStatus(projectItemId, state, session, context) {
473
672
  }
474
673
  });
475
674
  }
675
+ async function addStateLabel(issueId, state, session, context) {
676
+ const labelId = await resolveStateLabelId(state, session, context);
677
+ await context.client.graphql(ADD_LABELS_MUTATION, {
678
+ input: {
679
+ labelableId: issueId,
680
+ labelIds: [labelId]
681
+ }
682
+ });
683
+ }
684
+ async function updateIssueStateLabel(id, listName, state, session, context) {
685
+ const issueNumber = parseIssueNumber(id, listName);
686
+ let after;
687
+ let issue = null;
688
+ let isInProject = session.projectId === undefined;
689
+ while (true) {
690
+ const result = await context.client.graphql(session.projectId === undefined
691
+ ? REPOSITORY_ISSUE_STATE_LABELS_QUERY
692
+ : ISSUE_STATE_LABELS_QUERY, {
693
+ owner: context.repoOwner,
694
+ repo: context.repoName,
695
+ number: issueNumber,
696
+ after
697
+ });
698
+ const currentIssue = result.repository?.issue ?? null;
699
+ if (currentIssue === null) {
700
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
701
+ }
702
+ if (typeof currentIssue.id === "string") {
703
+ context.issueIds.set(issueNumber, currentIssue.id);
704
+ }
705
+ issue ??= currentIssue;
706
+ if (session.projectId === undefined) {
707
+ break;
708
+ }
709
+ if (currentIssue.projectItems?.nodes?.some((item) => item.project?.id === session.projectId)) {
710
+ isInProject = true;
711
+ break;
712
+ }
713
+ const pageInfo = currentIssue.projectItems?.pageInfo;
714
+ if (pageInfo?.hasNextPage !== true || pageInfo.endCursor === undefined || pageInfo.endCursor === null) {
715
+ break;
716
+ }
717
+ after = pageInfo.endCursor;
718
+ }
719
+ if (issue === null) {
720
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
721
+ }
722
+ const issueId = issue.id ?? null;
723
+ if (issueId === null) {
724
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
725
+ }
726
+ if (!isInProject) {
727
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
728
+ }
729
+ const targetLabel = `${session.labelPrefix}${state}`;
730
+ const stateLabels = (issue.labels?.nodes ?? []).filter((node) => node !== null && node.name.startsWith(session.labelPrefix ?? ""));
731
+ const targetNode = stateLabels.find((node) => node.name === targetLabel);
732
+ if (targetNode === undefined) {
733
+ await addStateLabel(issueId, state, session, context);
734
+ }
735
+ const labelIdsToRemove = stateLabels
736
+ .filter((node) => node.name !== targetLabel && node.id !== undefined)
737
+ .map((node) => node.id);
738
+ if (labelIdsToRemove.length > 0) {
739
+ await context.client.graphql(REMOVE_LABELS_MUTATION, {
740
+ input: {
741
+ labelableId: issueId,
742
+ labelIds: labelIdsToRemove
743
+ }
744
+ });
745
+ }
746
+ }
747
+ async function resolveStateLabelId(state, session, context) {
748
+ return resolveLabelId(`${session.labelPrefix}${state}`, session, context);
749
+ }
750
+ async function resolveConfiguredLabelIds(session, context) {
751
+ return Promise.all((context.labels ?? []).map((name) => resolveLabelId(name, session, context)));
752
+ }
753
+ async function resolveLabelId(name, session, context) {
754
+ const cachedLabelId = session.labelIds.get(name);
755
+ if (cachedLabelId !== undefined) {
756
+ return cachedLabelId;
757
+ }
758
+ const result = await context.client.graphql(REPOSITORY_LABEL_QUERY, {
759
+ owner: context.repoOwner,
760
+ repo: context.repoName,
761
+ name
762
+ });
763
+ const labelId = result.repository?.label?.id ?? null;
764
+ if (labelId === null) {
765
+ throw new Error(`GitHub label "${name}" not found or inaccessible.`);
766
+ }
767
+ session.labelIds.set(name, labelId);
768
+ return labelId;
769
+ }
476
770
  async function updateProjectItemPosition(projectItemId, afterId, session, context) {
771
+ assertProjectBacked(session, "update project position");
477
772
  await context.client.graphql(UPDATE_PROJECT_ITEM_POSITION_MUTATION, {
478
773
  input: {
479
774
  projectId: session.projectId,
@@ -524,6 +819,7 @@ async function resolveMoveAfterId(movingId, anchor, listName, session, context)
524
819
  return null;
525
820
  }
526
821
  async function fetchProjectTasks(listName, session, context) {
822
+ assertProjectBacked(session, "list project items");
527
823
  const tasks = [];
528
824
  let after = null;
529
825
  do {
@@ -542,27 +838,67 @@ async function fetchProjectTasks(listName, session, context) {
542
838
  } while (after !== null);
543
839
  return tasks;
544
840
  }
841
+ async function fetchTasks(listName, session, context) {
842
+ if (session.projectId !== undefined) {
843
+ return fetchProjectTasks(listName, session, context);
844
+ }
845
+ const tasks = [];
846
+ let after = null;
847
+ do {
848
+ const result = await context.client.graphql(REPOSITORY_ISSUES_QUERY, {
849
+ owner: context.repoOwner,
850
+ repo: context.repoName,
851
+ labels: context.labels,
852
+ after
853
+ });
854
+ const issues = result.repository?.issues;
855
+ for (const issue of issues?.nodes ?? []) {
856
+ tasks.push(mapIssueToTask({ issue, statusName: null, listName, session }));
857
+ }
858
+ after = issues?.pageInfo?.hasNextPage === true ? (issues.pageInfo.endCursor ?? null) : null;
859
+ } while (after !== null);
860
+ return tasks;
861
+ }
545
862
  async function fetchIssueTask(id, listName, session, context) {
546
863
  const issueNumber = parseIssueNumber(id, listName);
547
- const result = await context.client.graphql(ISSUE_QUERY, {
548
- owner: context.repoOwner,
549
- repo: context.repoName,
550
- number: issueNumber
551
- });
552
- const issue = result.repository?.issue ?? null;
553
- if (issue === null) {
554
- throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
555
- }
556
- const projectItem = issue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
557
- if (projectItem === null) {
558
- throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
864
+ let after;
865
+ let issue = null;
866
+ let projectItem = null;
867
+ while (true) {
868
+ const result = await context.client.graphql(session.projectId === undefined ? REPOSITORY_ISSUE_QUERY : ISSUE_QUERY, {
869
+ owner: context.repoOwner,
870
+ repo: context.repoName,
871
+ number: issueNumber,
872
+ after
873
+ });
874
+ const currentIssue = result.repository?.issue ?? null;
875
+ if (currentIssue === null) {
876
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
877
+ }
878
+ if (currentIssue.id !== undefined) {
879
+ context.issueIds.set(issueNumber, currentIssue.id);
880
+ }
881
+ issue ??= currentIssue;
882
+ if (session.projectId === undefined) {
883
+ break;
884
+ }
885
+ projectItem =
886
+ currentIssue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
887
+ if (projectItem !== null) {
888
+ break;
889
+ }
890
+ const pageInfo = currentIssue.projectItems?.pageInfo;
891
+ if (pageInfo?.hasNextPage !== true || pageInfo.endCursor === undefined || pageInfo.endCursor === null) {
892
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
893
+ }
894
+ after = pageInfo.endCursor;
559
895
  }
560
896
  return mapIssueToTask({
561
897
  issue,
562
- projectItemId: projectItem.id,
563
- statusName: projectItem.fieldValueByName?.name ?? null,
898
+ projectItemId: projectItem?.id,
899
+ statusName: projectItem?.fieldValueByName?.name ?? null,
564
900
  listName,
565
- initialState: session.stateMachine.initial
901
+ session
566
902
  });
567
903
  }
568
904
  function parseIssueNumber(id, listName) {
@@ -582,7 +918,7 @@ function mapProjectItemToTask(item, listName, session) {
582
918
  projectItemId: item.id,
583
919
  statusName: item.fieldValueByName?.name ?? null,
584
920
  listName,
585
- initialState: session.stateMachine.initial
921
+ session
586
922
  });
587
923
  }
588
924
  function isIssueNode(value) {
@@ -607,17 +943,23 @@ function mapIssueToTask(options) {
607
943
  qualifiedId: `${options.listName}#${id}`,
608
944
  name: options.issue.title,
609
945
  description: options.issue.body ?? "",
610
- state: options.statusName ?? options.initialState,
946
+ state: resolveTaskState(labels, options.statusName, options.session),
611
947
  metadata: {
612
948
  url: options.issue.url,
613
949
  labels,
614
950
  assignees,
615
951
  milestone: options.issue.milestone?.title ?? null,
616
- projectItemId: options.projectItemId,
952
+ ...(options.projectItemId === undefined ? {} : { projectItemId: options.projectItemId }),
617
953
  created: options.issue.createdAt
618
954
  }
619
955
  };
620
956
  }
957
+ function resolveTaskState(labels, statusName, session) {
958
+ if (session.labelPrefix === undefined) {
959
+ return statusName ?? session.stateMachine.initial;
960
+ }
961
+ return (session.stateMachine.states.find((state) => labels.includes(`${session.labelPrefix}${state}`)) ?? session.stateMachine.initial);
962
+ }
621
963
  function projectItemIdFromTask(task) {
622
964
  const projectItemId = task.metadata.projectItemId;
623
965
  if (typeof projectItemId !== "string") {
@@ -625,6 +967,25 @@ function projectItemIdFromTask(task) {
625
967
  }
626
968
  return projectItemId;
627
969
  }
970
+ function assertProjectBacked(session, operation) {
971
+ if (session.projectId === undefined || session.statusFieldId === undefined) {
972
+ throw new Error(`gh-issues ${operation} requires a configured GitHub Project.`);
973
+ }
974
+ }
975
+ function resolveLabelsFilter(filter) {
976
+ if (filter === undefined) {
977
+ return undefined;
978
+ }
979
+ const prefix = "label:";
980
+ if (!filter.startsWith(prefix)) {
981
+ throw new Error('gh-issues filter currently supports only "label:<name>".');
982
+ }
983
+ const label = filter.slice(prefix.length).trim();
984
+ if (label.length === 0) {
985
+ throw new Error('gh-issues filter requires a non-empty label after "label:".');
986
+ }
987
+ return [label];
988
+ }
628
989
  function parseRepo(repo) {
629
990
  const parts = repo.split("/");
630
991
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {