paperclip-github-plugin 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/worker.ts
2
2
  import { Buffer } from "node:buffer";
3
3
  import { realpathSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
4
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
- import { join, resolve } from "node:path";
6
+ import { dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { Octokit } from "@octokit/rest";
9
9
  import {
@@ -1604,6 +1604,10 @@ function getErrorMessage(error) {
1604
1604
  }
1605
1605
  return String(error);
1606
1606
  }
1607
+ function isPluginSecretReferenceDisabledError(error) {
1608
+ const message = getErrorMessage(error).toLowerCase();
1609
+ return message.includes("plugin secret reference") && message.includes("disabled") || message.includes("company-scoped plugin config lands");
1610
+ }
1607
1611
  function getErrorCause(error) {
1608
1612
  if (!error || typeof error !== "object" || !("cause" in error)) {
1609
1613
  return void 0;
@@ -1974,6 +1978,34 @@ function normalizeGitHubTokenRefs(value) {
1974
1978
  }
1975
1979
  return Object.fromEntries(entries);
1976
1980
  }
1981
+ function normalizeGitHubTokensByCompanyId(value) {
1982
+ if (!value || typeof value !== "object") {
1983
+ return void 0;
1984
+ }
1985
+ const entries = Object.entries(value).map(([companyId, token]) => {
1986
+ const normalizedCompanyId = normalizeCompanyId(companyId);
1987
+ const normalizedToken = normalizeGitHubToken(token);
1988
+ return normalizedCompanyId && normalizedToken ? [normalizedCompanyId, normalizedToken] : null;
1989
+ }).filter((entry) => entry !== null);
1990
+ if (entries.length === 0) {
1991
+ return void 0;
1992
+ }
1993
+ return Object.fromEntries(entries);
1994
+ }
1995
+ function normalizePaperclipBoardApiTokensByCompanyId(value) {
1996
+ if (!value || typeof value !== "object") {
1997
+ return void 0;
1998
+ }
1999
+ const entries = Object.entries(value).map(([companyId, token]) => {
2000
+ const normalizedCompanyId = normalizeCompanyId(companyId);
2001
+ const normalizedToken = normalizeGitHubToken(token);
2002
+ return normalizedCompanyId && normalizedToken ? [normalizedCompanyId, normalizedToken] : null;
2003
+ }).filter((entry) => entry !== null);
2004
+ if (entries.length === 0) {
2005
+ return void 0;
2006
+ }
2007
+ return Object.fromEntries(entries);
2008
+ }
1977
2009
  function formatUtcTimestamp(value) {
1978
2010
  const parsed = new Date(value);
1979
2011
  if (Number.isNaN(parsed.getTime())) {
@@ -3710,13 +3742,17 @@ function normalizeConfig(value) {
3710
3742
  const githubTokenRefs = normalizeGitHubTokenRefs(record.githubTokenRefs);
3711
3743
  const githubTokenRef = normalizeGitHubTokenRef(record.githubTokenRef);
3712
3744
  const githubToken = normalizeGitHubToken(record.githubToken);
3745
+ const githubTokensByCompanyId = normalizeGitHubTokensByCompanyId(record.githubTokensByCompanyId);
3713
3746
  const paperclipBoardApiTokenRefs = normalizePaperclipBoardApiTokenRefs(record.paperclipBoardApiTokenRefs);
3747
+ const paperclipBoardApiTokensByCompanyId = normalizePaperclipBoardApiTokensByCompanyId(record.paperclipBoardApiTokensByCompanyId);
3714
3748
  const paperclipApiBaseUrl = normalizePaperclipApiBaseUrl(record.paperclipApiBaseUrl);
3715
3749
  return {
3716
3750
  ...githubTokenRefs ? { githubTokenRefs } : {},
3717
3751
  ...githubTokenRef ? { githubTokenRef } : {},
3718
3752
  ...githubToken ? { githubToken } : {},
3753
+ ...githubTokensByCompanyId ? { githubTokensByCompanyId } : {},
3719
3754
  ...paperclipBoardApiTokenRefs ? { paperclipBoardApiTokenRefs } : {},
3755
+ ...paperclipBoardApiTokensByCompanyId ? { paperclipBoardApiTokensByCompanyId } : {},
3720
3756
  ...paperclipApiBaseUrl ? { paperclipApiBaseUrl } : {}
3721
3757
  };
3722
3758
  }
@@ -3791,6 +3827,129 @@ async function readExternalConfig(ctx) {
3791
3827
  return {};
3792
3828
  }
3793
3829
  }
3830
+ async function readExternalConfigRecordForWrite(ctx, filePath) {
3831
+ try {
3832
+ const rawConfig = await readFile(filePath, "utf8");
3833
+ const parsedConfig = JSON.parse(rawConfig);
3834
+ return parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig) ? { ...parsedConfig } : {};
3835
+ } catch (error) {
3836
+ const errorCode = error && typeof error === "object" && "code" in error ? error.code : void 0;
3837
+ if (errorCode === "ENOENT") {
3838
+ return {};
3839
+ }
3840
+ if (error instanceof SyntaxError) {
3841
+ ctx.logger.warn("Ignoring the GitHub Sync worker-local token fallback config file because it is not valid JSON.", {
3842
+ filePath,
3843
+ error: error.message
3844
+ });
3845
+ return {};
3846
+ }
3847
+ throw error;
3848
+ }
3849
+ }
3850
+ async function writeExternalCompanyGitHubTokenFallback(ctx, companyId, token) {
3851
+ const externalConfigFilePath = getExternalConfigFilePath();
3852
+ if (!externalConfigFilePath) {
3853
+ throw new Error("Could not resolve a Paperclip home directory for the GitHub Sync fallback token config.");
3854
+ }
3855
+ const currentRecord = await readExternalConfigRecordForWrite(ctx, externalConfigFilePath);
3856
+ const currentCompanyTokens = normalizeGitHubTokensByCompanyId(currentRecord.githubTokensByCompanyId) ?? {};
3857
+ const nextRecord = {
3858
+ ...currentRecord,
3859
+ githubTokensByCompanyId: {
3860
+ ...currentCompanyTokens,
3861
+ [companyId]: token
3862
+ }
3863
+ };
3864
+ await mkdir(dirname(externalConfigFilePath), { recursive: true });
3865
+ await writeFile(externalConfigFilePath, `${JSON.stringify(nextRecord, null, 2)}
3866
+ `, {
3867
+ encoding: "utf8",
3868
+ mode: 384
3869
+ });
3870
+ try {
3871
+ await chmod(externalConfigFilePath, 384);
3872
+ } catch (error) {
3873
+ ctx.logger.warn("GitHub Sync could not tighten permissions on the worker-local token fallback file.", {
3874
+ filePath: externalConfigFilePath,
3875
+ error: getErrorMessage(error)
3876
+ });
3877
+ }
3878
+ }
3879
+ async function writeExternalCompanyPaperclipBoardApiTokenFallback(ctx, companyId, token) {
3880
+ const externalConfigFilePath = getExternalConfigFilePath();
3881
+ if (!externalConfigFilePath) {
3882
+ throw new Error("Could not resolve a Paperclip home directory for the GitHub Sync fallback token config.");
3883
+ }
3884
+ const currentRecord = await readExternalConfigRecordForWrite(ctx, externalConfigFilePath);
3885
+ const currentCompanyTokens = normalizePaperclipBoardApiTokensByCompanyId(currentRecord.paperclipBoardApiTokensByCompanyId) ?? {};
3886
+ const nextRecord = {
3887
+ ...currentRecord,
3888
+ paperclipBoardApiTokensByCompanyId: {
3889
+ ...currentCompanyTokens,
3890
+ [companyId]: token
3891
+ }
3892
+ };
3893
+ await mkdir(dirname(externalConfigFilePath), { recursive: true });
3894
+ await writeFile(externalConfigFilePath, `${JSON.stringify(nextRecord, null, 2)}
3895
+ `, {
3896
+ encoding: "utf8",
3897
+ mode: 384
3898
+ });
3899
+ try {
3900
+ await chmod(externalConfigFilePath, 384);
3901
+ } catch (error) {
3902
+ ctx.logger.warn("GitHub Sync could not tighten permissions on the worker-local token fallback file.", {
3903
+ filePath: externalConfigFilePath,
3904
+ error: getErrorMessage(error)
3905
+ });
3906
+ }
3907
+ }
3908
+ async function clearExternalCompanyPaperclipBoardApiTokenFallback(ctx, companyId) {
3909
+ const externalConfigFilePath = getExternalConfigFilePath();
3910
+ if (!externalConfigFilePath) {
3911
+ return;
3912
+ }
3913
+ const currentRecord = await readExternalConfigRecordForWrite(ctx, externalConfigFilePath);
3914
+ const currentCompanyTokens = normalizePaperclipBoardApiTokensByCompanyId(currentRecord.paperclipBoardApiTokensByCompanyId) ?? {};
3915
+ if (!(companyId in currentCompanyTokens)) {
3916
+ return;
3917
+ }
3918
+ const nextCompanyTokens = { ...currentCompanyTokens };
3919
+ delete nextCompanyTokens[companyId];
3920
+ const nextRecord = { ...currentRecord };
3921
+ if (Object.keys(nextCompanyTokens).length > 0) {
3922
+ nextRecord.paperclipBoardApiTokensByCompanyId = nextCompanyTokens;
3923
+ } else {
3924
+ delete nextRecord.paperclipBoardApiTokensByCompanyId;
3925
+ }
3926
+ await mkdir(dirname(externalConfigFilePath), { recursive: true });
3927
+ await writeFile(externalConfigFilePath, `${JSON.stringify(nextRecord, null, 2)}
3928
+ `, {
3929
+ encoding: "utf8",
3930
+ mode: 384
3931
+ });
3932
+ try {
3933
+ await chmod(externalConfigFilePath, 384);
3934
+ } catch (error) {
3935
+ ctx.logger.warn("GitHub Sync could not tighten permissions on the worker-local token fallback file.", {
3936
+ filePath: externalConfigFilePath,
3937
+ error: getErrorMessage(error)
3938
+ });
3939
+ }
3940
+ }
3941
+ async function shouldSeedExternalPaperclipBoardTokenFallback(ctx, companyId, secretRef) {
3942
+ try {
3943
+ return !(await ctx.secrets.resolve(secretRef)).trim();
3944
+ } catch (error) {
3945
+ ctx.logger.warn("Unable to resolve the saved Paperclip board API token while checking worker fallback necessity.", {
3946
+ companyId,
3947
+ secretRef,
3948
+ error: getErrorMessage(error)
3949
+ });
3950
+ return true;
3951
+ }
3952
+ }
3794
3953
  function normalizePaperclipBoardApiTokenRefs(value) {
3795
3954
  if (!value || typeof value !== "object") {
3796
3955
  return void 0;
@@ -9985,6 +10144,7 @@ async function getResolvedConfig(ctx) {
9985
10144
  }
9986
10145
  function getConfiguredGithubTokenSource(settings, config, companyId) {
9987
10146
  const normalizedCompanyId = normalizeCompanyId(companyId);
10147
+ const companyFallbackToken = normalizedCompanyId ? normalizeGitHubToken(config.githubTokensByCompanyId?.[normalizedCompanyId]) : void 0;
9988
10148
  const hasScopedGitHubTokenRefs = hasAnyScopedValue(settings?.githubTokenRefs) || hasAnyScopedValue(config.githubTokenRefs);
9989
10149
  const secretRef = normalizedCompanyId ? normalizeSecretRef(config.githubTokenRefs?.[normalizedCompanyId]) ?? normalizeSecretRef(settings?.githubTokenRefs?.[normalizedCompanyId]) ?? (!hasScopedGitHubTokenRefs ? normalizeGitHubTokenRef(config.githubTokenRef) ?? normalizeGitHubTokenRef(settings?.githubTokenRef) : void 0) : normalizeGitHubTokenRef(config.githubTokenRef) ?? normalizeGitHubTokenRef(settings?.githubTokenRef) ?? (() => {
9990
10150
  const configuredRefs = [
@@ -9995,14 +10155,17 @@ function getConfiguredGithubTokenSource(settings, config, companyId) {
9995
10155
  return uniqueRefs.length === 1 ? uniqueRefs[0] : void 0;
9996
10156
  })();
9997
10157
  if (secretRef) {
9998
- return { secretRef };
10158
+ return {
10159
+ secretRef,
10160
+ ...companyFallbackToken ? { fallbackToken: companyFallbackToken } : {}
10161
+ };
9999
10162
  }
10000
- const token = !normalizedCompanyId || !hasScopedGitHubTokenRefs ? normalizeGitHubToken(config.githubToken) : void 0;
10163
+ const token = companyFallbackToken ?? (!normalizedCompanyId || !hasScopedGitHubTokenRefs ? normalizeGitHubToken(config.githubToken) : void 0);
10001
10164
  return token ? { token } : {};
10002
10165
  }
10003
10166
  function hasConfiguredGithubToken(settings, config, companyId) {
10004
10167
  const configuredTokenSource = getConfiguredGithubTokenSource(settings, config, companyId);
10005
- if (configuredTokenSource.secretRef ?? configuredTokenSource.token) {
10168
+ if (configuredTokenSource.secretRef ?? configuredTokenSource.token ?? configuredTokenSource.fallbackToken) {
10006
10169
  return true;
10007
10170
  }
10008
10171
  if (normalizeCompanyId(companyId)) {
@@ -10012,6 +10175,18 @@ function hasConfiguredGithubToken(settings, config, companyId) {
10012
10175
  settings?.githubTokenRefs && Object.keys(settings.githubTokenRefs).length > 0 || config.githubTokenRefs && Object.keys(config.githubTokenRefs).length > 0
10013
10176
  );
10014
10177
  }
10178
+ function getSavedGitHubTokenRef(settings, companyId) {
10179
+ if (!companyId) {
10180
+ return void 0;
10181
+ }
10182
+ return normalizeSecretRef(settings?.githubTokenRefs?.[companyId]);
10183
+ }
10184
+ function getConfiguredGitHubTokenRef(config, companyId) {
10185
+ if (!companyId) {
10186
+ return void 0;
10187
+ }
10188
+ return normalizeSecretRef(config?.githubTokenRefs?.[companyId]);
10189
+ }
10015
10190
  function getSavedPaperclipBoardApiTokenRef(settings, companyId) {
10016
10191
  if (!companyId) {
10017
10192
  return void 0;
@@ -10061,11 +10236,16 @@ async function resolvePaperclipApiAuthTokens(ctx, settings, config, mappings) {
10061
10236
  for (const companyId of companyIds) {
10062
10237
  const configuredSecretRef = getConfiguredPaperclipBoardApiTokenRef(config, companyId);
10063
10238
  const savedSecretRef = getSavedPaperclipBoardApiTokenRef(settings, companyId);
10239
+ const fallbackToken = normalizeGitHubToken(config.paperclipBoardApiTokensByCompanyId?.[companyId]);
10064
10240
  const secretRef = configuredSecretRef ?? savedSecretRef;
10065
10241
  if (!secretRef) {
10066
10242
  continue;
10067
10243
  }
10068
10244
  if (!configuredSecretRef && savedSecretRef) {
10245
+ if (fallbackToken) {
10246
+ tokensByCompanyId.set(companyId, fallbackToken);
10247
+ continue;
10248
+ }
10069
10249
  ctx.logger.warn(
10070
10250
  "Paperclip board access is saved in plugin state but has not been mirrored into plugin config yet. Open plugin settings to finish migrating it, or reconnect board access, before retrying sync.",
10071
10251
  {
@@ -10081,6 +10261,15 @@ async function resolvePaperclipApiAuthTokens(ctx, settings, config, mappings) {
10081
10261
  tokensByCompanyId.set(companyId, token);
10082
10262
  }
10083
10263
  } catch (error) {
10264
+ if (fallbackToken && isPluginSecretReferenceDisabledError(error)) {
10265
+ ctx.logger.warn("GitHub Sync is using a worker-local Paperclip board token fallback because plugin secret refs are unavailable in this host.", {
10266
+ companyId,
10267
+ secretRef,
10268
+ error: getErrorMessage(error)
10269
+ });
10270
+ tokensByCompanyId.set(companyId, fallbackToken);
10271
+ continue;
10272
+ }
10084
10273
  ctx.logger.warn("Unable to resolve the saved Paperclip board API token. Direct REST calls will continue without it.", {
10085
10274
  companyId,
10086
10275
  secretRef,
@@ -10095,7 +10284,23 @@ async function resolveGithubToken(ctx, options = {}) {
10095
10284
  const config = options.config ?? await getResolvedConfig(ctx);
10096
10285
  const configuredTokenSource = getConfiguredGithubTokenSource(settings, config, options.companyId);
10097
10286
  if (configuredTokenSource.secretRef) {
10098
- return ctx.secrets.resolve(configuredTokenSource.secretRef);
10287
+ try {
10288
+ const token = (await ctx.secrets.resolve(configuredTokenSource.secretRef)).trim();
10289
+ if (token) {
10290
+ return token;
10291
+ }
10292
+ return configuredTokenSource.fallbackToken ?? "";
10293
+ } catch (error) {
10294
+ if (configuredTokenSource.fallbackToken && isPluginSecretReferenceDisabledError(error)) {
10295
+ ctx.logger.warn("GitHub Sync is using a worker-local company token fallback because plugin secret refs are unavailable in this host.", {
10296
+ companyId: normalizeCompanyId(options.companyId),
10297
+ secretRef: configuredTokenSource.secretRef,
10298
+ error: getErrorMessage(error)
10299
+ });
10300
+ return configuredTokenSource.fallbackToken;
10301
+ }
10302
+ throw error;
10303
+ }
10099
10304
  }
10100
10305
  return configuredTokenSource.token ?? "";
10101
10306
  }
@@ -15553,6 +15758,8 @@ var __testing = {
15553
15758
  formatPaperclipApiFetchErrorMessage,
15554
15759
  hasUnresolvedPaperclipIssueBlocker,
15555
15760
  isHealthyMaintainerWaitTransition,
15761
+ resolvePaperclipApiAuthTokens,
15762
+ resolveGithubToken,
15556
15763
  resolvePaperclipPullRequestIssueStatus,
15557
15764
  resolveSyncTransitionAssignee
15558
15765
  };
@@ -15569,6 +15776,8 @@ var plugin = definePlugin({
15569
15776
  const normalizedSettings = normalizeSettings(saved);
15570
15777
  const config = await getResolvedConfig(ctx);
15571
15778
  const githubTokenConfigured = hasConfiguredGithubToken(normalizedSettings, config, requestedCompanyId);
15779
+ const configuredGitHubTokenRef = getConfiguredGitHubTokenRef(config, requestedCompanyId);
15780
+ const savedGitHubTokenRef = getSavedGitHubTokenRef(normalizedSettings, requestedCompanyId);
15572
15781
  const configuredBoardTokenRef = getConfiguredPaperclipBoardApiTokenRef(config, requestedCompanyId);
15573
15782
  const savedBoardTokenRef = getSavedPaperclipBoardApiTokenRef(normalizedSettings, requestedCompanyId);
15574
15783
  const settingsForResponse = sanitizeSettingsForCurrentSetup(
@@ -15590,6 +15799,8 @@ var plugin = definePlugin({
15590
15799
  paperclipApiBaseUrlConfigured: Boolean(normalizePaperclipApiBaseUrl(config.paperclipApiBaseUrl)),
15591
15800
  githubTokenConfigured,
15592
15801
  paperclipBoardAccessConfigured: requestedCompanyId ? hasConfiguredPaperclipBoardAccess(settingsForResponse, config, requestedCompanyId) : hasConfiguredPaperclipBoardAccessForMappings(settingsForResponse, config, scopedMappings),
15802
+ ...savedGitHubTokenRef ? { githubTokenConfigSyncRef: savedGitHubTokenRef } : {},
15803
+ githubTokenNeedsConfigSync: Boolean(savedGitHubTokenRef && configuredGitHubTokenRef !== savedGitHubTokenRef),
15593
15804
  ...savedBoardTokenRef ? { paperclipBoardAccessConfigSyncRef: savedBoardTokenRef } : {},
15594
15805
  paperclipBoardAccessNeedsConfigSync: Boolean(savedBoardTokenRef && !configuredBoardTokenRef)
15595
15806
  };
@@ -15792,6 +16003,7 @@ var plugin = definePlugin({
15792
16003
  throw new Error("A company id is required to update Paperclip board access.");
15793
16004
  }
15794
16005
  const nextSecretRef = normalizeSecretRef(record.paperclipBoardApiTokenRef);
16006
+ const nextBoardApiToken = normalizeGitHubToken(record.paperclipBoardApiToken);
15795
16007
  const nextPaperclipBoardApiTokenRefs = {
15796
16008
  ...previous.paperclipBoardApiTokenRefs ?? {}
15797
16009
  };
@@ -15803,10 +16015,18 @@ var plugin = definePlugin({
15803
16015
  };
15804
16016
  if (nextSecretRef) {
15805
16017
  nextPaperclipBoardApiTokenRefs[companyId] = nextSecretRef;
16018
+ if (nextBoardApiToken) {
16019
+ if (await shouldSeedExternalPaperclipBoardTokenFallback(ctx, companyId, nextSecretRef)) {
16020
+ await writeExternalCompanyPaperclipBoardApiTokenFallback(ctx, companyId, nextBoardApiToken);
16021
+ } else {
16022
+ await clearExternalCompanyPaperclipBoardApiTokenFallback(ctx, companyId);
16023
+ }
16024
+ }
15806
16025
  } else {
15807
16026
  delete nextPaperclipBoardApiTokenRefs[companyId];
15808
16027
  delete nextPaperclipBoardAccessIdentityByCompanyId[companyId];
15809
16028
  delete nextPaperclipBoardAccessUserIdByCompanyId[companyId];
16029
+ await clearExternalCompanyPaperclipBoardApiTokenFallback(ctx, companyId);
15810
16030
  }
15811
16031
  if ("paperclipBoardAccessIdentity" in record) {
15812
16032
  const nextIdentityLabel = normalizeOptionalString2(record.paperclipBoardAccessIdentity);
@@ -15856,6 +16076,44 @@ var plugin = definePlugin({
15856
16076
  }
15857
16077
  return validateGithubToken(ctx, trimmedToken);
15858
16078
  });
16079
+ ctx.actions.register("settings.ensureGitHubTokenAvailable", async (input) => {
16080
+ const record = input && typeof input === "object" ? input : {};
16081
+ const companyId = normalizeCompanyId(record.companyId);
16082
+ const githubTokenRef = normalizeSecretRef(record.githubTokenRef);
16083
+ const token = normalizeGitHubToken(record.token);
16084
+ if (!companyId) {
16085
+ throw new Error("Company context is required to verify worker access to the GitHub token.");
16086
+ }
16087
+ if (!githubTokenRef) {
16088
+ throw new Error("A GitHub token secret ref is required to verify worker token access.");
16089
+ }
16090
+ if (!token) {
16091
+ throw new Error("A validated GitHub token is required to prepare the worker token fallback.");
16092
+ }
16093
+ try {
16094
+ const resolvedToken = (await ctx.secrets.resolve(githubTokenRef)).trim();
16095
+ if (resolvedToken) {
16096
+ return {
16097
+ secretResolvable: true,
16098
+ fallbackStored: false
16099
+ };
16100
+ }
16101
+ } catch (error) {
16102
+ if (!isPluginSecretReferenceDisabledError(error)) {
16103
+ throw error;
16104
+ }
16105
+ await writeExternalCompanyGitHubTokenFallback(ctx, companyId, token);
16106
+ return {
16107
+ secretResolvable: false,
16108
+ fallbackStored: true
16109
+ };
16110
+ }
16111
+ await writeExternalCompanyGitHubTokenFallback(ctx, companyId, token);
16112
+ return {
16113
+ secretResolvable: false,
16114
+ fallbackStored: true
16115
+ };
16116
+ });
15859
16117
  ctx.actions.register("project.pullRequests.createIssue", async (input) => {
15860
16118
  const record = input && typeof input === "object" ? input : {};
15861
16119
  return createProjectPullRequestPaperclipIssue(ctx, record);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",