@pachca/cli 2026.10.1 → 2026.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +24 -24
- package/README.template.md +14 -23
- package/dist/base-command.js +1 -1
- package/dist/client.js +56 -32
- package/dist/commands/api.js +1 -1
- package/dist/commands/auth/logout.js +23 -12
- package/dist/commands/bots/create-token.js +0 -3
- package/dist/commands/bots/create.js +0 -4
- package/dist/commands/bots/delete.js +0 -3
- package/dist/commands/bots/get-stream.js +23 -0
- package/dist/commands/bots/list-company.js +0 -3
- package/dist/commands/bots/list-events.js +0 -3
- package/dist/commands/bots/list-scopes.js +0 -3
- package/dist/commands/bots/list.js +0 -3
- package/dist/commands/bots/recreate-token-self.js +0 -3
- package/dist/commands/bots/recreate-token.js +0 -3
- package/dist/commands/bots/reissue-token.js +0 -3
- package/dist/commands/bots/remove-event.js +0 -3
- package/dist/commands/bots/update-webhook.js +0 -3
- package/dist/commands/bots/update.js +0 -5
- package/dist/commands/chats/archive.js +0 -4
- package/dist/commands/chats/create.js +3 -6
- package/dist/commands/chats/download-export.js +0 -3
- package/dist/commands/chats/list-company.js +0 -3
- package/dist/commands/chats/list.js +1 -6
- package/dist/commands/chats/remove-avatar.js +0 -3
- package/dist/commands/chats/request-export.js +0 -3
- package/dist/commands/chats/unarchive.js +0 -3
- package/dist/commands/chats/update-avatar.js +0 -3
- package/dist/commands/chats/update.js +0 -3
- package/dist/commands/custom-properties/list.js +0 -3
- package/dist/commands/drafts/create.js +1 -4
- package/dist/commands/drafts/delete.js +0 -3
- package/dist/commands/drafts/update.js +1 -1
- package/dist/commands/files/uploads.js +0 -3
- package/dist/commands/group-tags/create.js +0 -3
- package/dist/commands/group-tags/list-users.js +0 -3
- package/dist/commands/group-tags/list.js +0 -3
- package/dist/commands/important-messages/create.js +64 -0
- package/dist/commands/important-messages/delete.js +42 -0
- package/dist/commands/important-messages/list.js +89 -0
- package/dist/commands/members/add.js +0 -5
- package/dist/commands/members/leave.js +0 -3
- package/dist/commands/members/list.js +0 -4
- package/dist/commands/members/remove.js +0 -3
- package/dist/commands/members/update.js +0 -3
- package/dist/commands/messages/create.js +3 -8
- package/dist/commands/messages/delete.js +0 -3
- package/dist/commands/messages/get.js +0 -5
- package/dist/commands/messages/list.js +1 -5
- package/dist/commands/messages/pin.js +0 -3
- package/dist/commands/messages/unfurl.js +0 -3
- package/dist/commands/messages/unpin.js +0 -3
- package/dist/commands/messages/update.js +2 -7
- package/dist/commands/oauth/token-info.js +0 -3
- package/dist/commands/profile/delete-avatar.js +0 -3
- package/dist/commands/profile/delete-status.js +0 -3
- package/dist/commands/profile/get.js +0 -4
- package/dist/commands/profile/update-avatar.js +0 -3
- package/dist/commands/profile/update-status.js +1 -4
- package/dist/commands/reactions/add.js +3 -6
- package/dist/commands/reactions/remove.js +3 -6
- package/dist/commands/read-member/list-readers.js +0 -3
- package/dist/commands/search/list-chats.js +0 -5
- package/dist/commands/search/list-messages.js +0 -3
- package/dist/commands/search/list-users.js +0 -5
- package/dist/commands/security/list.js +1 -6
- package/dist/commands/tasks/create.js +1 -6
- package/dist/commands/tasks/delete.js +0 -3
- package/dist/commands/tasks/get.js +0 -3
- package/dist/commands/tasks/list.js +0 -5
- package/dist/commands/tasks/update.js +2 -6
- package/dist/commands/threads/add.js +0 -4
- package/dist/commands/threads/list.js +0 -4
- package/dist/commands/users/create.js +0 -5
- package/dist/commands/users/delete.js +0 -3
- package/dist/commands/users/get-status.js +0 -3
- package/dist/commands/users/get.js +0 -3
- package/dist/commands/users/list.js +0 -5
- package/dist/commands/users/remove-avatar.js +0 -3
- package/dist/commands/users/remove-status.js +0 -3
- package/dist/commands/users/update-avatar.js +0 -3
- package/dist/commands/users/update-status.js +1 -4
- package/dist/commands/users/update.js +4 -4
- package/dist/commands/views/open.js +0 -5
- package/dist/commands/views/submit-response.js +0 -3
- package/dist/data/alternatives.json +5 -9
- package/dist/data/changelog.json +77 -0
- package/dist/data/endpoints.json +1 -1
- package/dist/help.js +0 -1
- package/dist/hooks/command-not-found.js +0 -1
- package/dist/hooks/init.js +5 -1
- package/dist/oauth.js +9 -4
- package/dist/token-refresh.js +61 -14
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/commands/guide.js +0 -90
- package/dist/data/workflows.json +0 -1058
package/dist/help.js
CHANGED
package/dist/hooks/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getDefaults, resolveToken, TokenNotFoundError, ProfileNotFoundError, }
|
|
|
8
8
|
import { ensureFreshToken, isSessionExpired } from '../token-refresh.js';
|
|
9
9
|
import { outputError } from '../output.js';
|
|
10
10
|
import { defaultOutputFormat, isInteractive } from '../utils.js';
|
|
11
|
+
import { detectIntrospectMode } from '../api-introspect.js';
|
|
11
12
|
/**
|
|
12
13
|
* Resolve output format from argv before flags are parsed by oclif.
|
|
13
14
|
*/
|
|
@@ -40,7 +41,6 @@ const SKIP_AUTH = new Set([
|
|
|
40
41
|
'doctor',
|
|
41
42
|
'autocomplete',
|
|
42
43
|
'commands',
|
|
43
|
-
'guide',
|
|
44
44
|
'introspect',
|
|
45
45
|
'changelog',
|
|
46
46
|
'upgrade',
|
|
@@ -102,6 +102,10 @@ const hook = async function (opts) {
|
|
|
102
102
|
// Skip auth for exempt commands
|
|
103
103
|
if (SKIP_AUTH.has(commandId))
|
|
104
104
|
return;
|
|
105
|
+
// `api ls` and `api <METHOD> <path> --describe|--spec|--docs` read the bundled
|
|
106
|
+
// spec and never call the API, so an agent can study the API before signing in.
|
|
107
|
+
if (commandId === 'api' && (opts.argv[0] === 'ls' || detectIntrospectMode(opts.argv)))
|
|
108
|
+
return;
|
|
105
109
|
// Skip auth when --help, --version, or --dry-run flag is passed
|
|
106
110
|
if (process.argv.includes('--help') || process.argv.includes('-h'))
|
|
107
111
|
return;
|
package/dist/oauth.js
CHANGED
|
@@ -162,10 +162,15 @@ export async function pollForToken(grant) {
|
|
|
162
162
|
/**
|
|
163
163
|
* Put out an access token server-side (RFC 7009).
|
|
164
164
|
*
|
|
165
|
-
* The endpoint only accepts self-revocation: the same token must arrive
|
|
166
|
-
* the body and as the bearer, and it must belong to the CLI's public
|
|
167
|
-
*
|
|
168
|
-
*
|
|
165
|
+
* The endpoint only accepts self-revocation: the same access token must arrive
|
|
166
|
+
* both in the body and as the bearer, and it must belong to the CLI's public
|
|
167
|
+
* client. A refresh token is refused here: the CLI does not know the client's
|
|
168
|
+
* real `client_id`, and self-revocation compares the body with the bearer.
|
|
169
|
+
*
|
|
170
|
+
* `true` proves the revocation only for a live token. The server answers `200`
|
|
171
|
+
* to an expired one too, yet leaves its record and refresh token untouched.
|
|
172
|
+
* Pass a token known to be live — `revokeSession` in `token-refresh.ts` does.
|
|
173
|
+
* Returns false when the server declined or could not be reached.
|
|
169
174
|
*/
|
|
170
175
|
export async function revokeToken(token) {
|
|
171
176
|
try {
|
package/dist/token-refresh.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { getConfigDirPath, getProfile, isExpiring, isRefreshable, updateProfileTokens, expiresAtFromNow, } from './profiles.js';
|
|
4
|
-
import { OAuthError, refreshAccessToken } from './oauth.js';
|
|
4
|
+
import { OAuthError, refreshAccessToken, revokeToken } from './oauth.js';
|
|
5
5
|
const LOCK_FILENAME = 'refresh.lock';
|
|
6
6
|
/** A lock older than this belonged to a process that died mid-refresh. */
|
|
7
7
|
const LOCK_STALE_MS = 30_000;
|
|
@@ -65,46 +65,46 @@ function releaseLock(fd) {
|
|
|
65
65
|
* Returns the profile to use. Throws `OAuthError` only when the session is truly
|
|
66
66
|
* dead and the user has to log in again.
|
|
67
67
|
*/
|
|
68
|
-
export async function ensureFreshToken(profileName, profile) {
|
|
69
|
-
if (!isRefreshable(profile) || !
|
|
68
|
+
export async function ensureFreshToken(profileName, profile, needsRefresh = isExpiring) {
|
|
69
|
+
if (!isRefreshable(profile) || !needsRefresh(profile))
|
|
70
70
|
return profile;
|
|
71
71
|
const fd = acquireLock();
|
|
72
72
|
if (fd === null) {
|
|
73
|
-
const waited = await waitForOtherProcess(profileName);
|
|
73
|
+
const waited = await waitForOtherProcess(profileName, needsRefresh);
|
|
74
74
|
if (waited)
|
|
75
75
|
return waited;
|
|
76
76
|
// The holder gave up or is stuck — refresh ourselves rather than fail.
|
|
77
|
-
return refreshAndStore(profileName, profile);
|
|
77
|
+
return refreshAndStore(profileName, profile, needsRefresh);
|
|
78
78
|
}
|
|
79
79
|
try {
|
|
80
80
|
// Re-read: the previous holder may have refreshed while we queued for the lock.
|
|
81
81
|
const current = getProfile(profileName) ?? profile;
|
|
82
|
-
if (!
|
|
82
|
+
if (!needsRefresh(current))
|
|
83
83
|
return current;
|
|
84
|
-
return await refreshAndStore(profileName, current);
|
|
84
|
+
return await refreshAndStore(profileName, current, needsRefresh);
|
|
85
85
|
}
|
|
86
86
|
finally {
|
|
87
87
|
releaseLock(fd);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
async function waitForOtherProcess(profileName) {
|
|
90
|
+
async function waitForOtherProcess(profileName, needsRefresh) {
|
|
91
91
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
92
92
|
while (Date.now() < deadline) {
|
|
93
93
|
await sleep(LOCK_POLL_MS);
|
|
94
94
|
const current = getProfile(profileName);
|
|
95
|
-
if (current && !
|
|
95
|
+
if (current && !needsRefresh(current))
|
|
96
96
|
return current;
|
|
97
97
|
if (!fs.existsSync(lockPath())) {
|
|
98
98
|
// The holder finished between our read of the profile and this check. Look
|
|
99
99
|
// once more before refreshing ourselves: their new token is already on disk,
|
|
100
100
|
// and ours has just been spent.
|
|
101
101
|
const after = getProfile(profileName);
|
|
102
|
-
return after && !
|
|
102
|
+
return after && !needsRefresh(after) ? after : undefined;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
return undefined;
|
|
106
106
|
}
|
|
107
|
-
async function refreshAndStore(profileName, profile) {
|
|
107
|
+
async function refreshAndStore(profileName, profile, needsRefresh) {
|
|
108
108
|
let grant;
|
|
109
109
|
try {
|
|
110
110
|
grant = await refreshAccessToken(profile.refresh_token);
|
|
@@ -116,21 +116,68 @@ async function refreshAndStore(profileName, profile) {
|
|
|
116
116
|
// Without this check a lost race logs the user out for no reason.
|
|
117
117
|
if (error instanceof OAuthError && error.code === 'invalid_grant') {
|
|
118
118
|
const current = getProfile(profileName);
|
|
119
|
-
if (current?.token && !
|
|
119
|
+
if (current?.token && !needsRefresh(current))
|
|
120
120
|
return current;
|
|
121
121
|
}
|
|
122
122
|
throw error;
|
|
123
123
|
}
|
|
124
|
-
const
|
|
124
|
+
const tokens = {
|
|
125
125
|
token: grant.access_token,
|
|
126
126
|
refresh_token: grant.refresh_token,
|
|
127
127
|
expires_at: grant.expires_in ? expiresAtFromNow(grant.expires_in) : undefined,
|
|
128
128
|
scopes: grant.scope ? grant.scope.split(' ').filter(Boolean) : undefined,
|
|
129
|
+
};
|
|
130
|
+
const updated = updateProfileTokens(profileName, tokens);
|
|
131
|
+
// The profile can vanish from the config mid-refresh. The new token is still
|
|
132
|
+
// the one in force: hand it back with its expiry so a caller can revoke it.
|
|
133
|
+
return (updated ?? {
|
|
134
|
+
...profile,
|
|
135
|
+
token: tokens.token,
|
|
136
|
+
refresh_token: tokens.refresh_token ?? profile.refresh_token,
|
|
137
|
+
expires_at: tokens.expires_at ?? profile.expires_at,
|
|
129
138
|
});
|
|
130
|
-
return updated ?? { ...profile, token: grant.access_token };
|
|
131
139
|
}
|
|
132
140
|
/** True when the failure means the session is gone for good, not a hiccup. */
|
|
133
141
|
export function isSessionExpired(error) {
|
|
134
142
|
return (error instanceof OAuthError &&
|
|
135
143
|
['invalid_grant', 'invalid_request', 'invalid_client', 'unauthorized_client'].includes(error.code));
|
|
136
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* True only when the access token will outlive the request about to use it.
|
|
147
|
+
* Unknown expiry counts as not live: revoking is only proof when the token is.
|
|
148
|
+
*/
|
|
149
|
+
function hasLiveToken(profile) {
|
|
150
|
+
if (!profile.token || !profile.expires_at)
|
|
151
|
+
return false;
|
|
152
|
+
if (Number.isNaN(Date.parse(profile.expires_at)))
|
|
153
|
+
return false;
|
|
154
|
+
return !isExpiring(profile);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Close the profile's OAuth login on the server.
|
|
158
|
+
*
|
|
159
|
+
* The revoke endpoint answers `200` even when it skipped the token: an expired
|
|
160
|
+
* access token is not revocable, so the record stays as it was, refresh token
|
|
161
|
+
* included, and that refresh token can still mint new access tokens. Revoking
|
|
162
|
+
* by the refresh token itself is not open to the CLI: self-revocation takes only
|
|
163
|
+
* the access token that also came as the bearer. So the token sent is always a
|
|
164
|
+
* live one — refreshed first when the saved one has run out. The refresh retires
|
|
165
|
+
* the old record on the server, and the revoke then closes the new one.
|
|
166
|
+
*/
|
|
167
|
+
export async function revokeSession(profileName, profile) {
|
|
168
|
+
let current;
|
|
169
|
+
try {
|
|
170
|
+
current = await ensureFreshToken(profileName, profile, (p) => !hasLiveToken(p));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// `invalid_grant` on refresh: the token is revoked or gone on the server.
|
|
174
|
+
if (error instanceof OAuthError && error.code === 'invalid_grant')
|
|
175
|
+
return 'closed';
|
|
176
|
+
return 'open';
|
|
177
|
+
}
|
|
178
|
+
// No refresh token and the access token has run out, or it never came back
|
|
179
|
+
// from the OS store: there is nothing to revoke with that would prove anything.
|
|
180
|
+
if (!hasLiveToken(current))
|
|
181
|
+
return 'open';
|
|
182
|
+
return (await revokeToken(current.token)) ? 'revoked' : 'open';
|
|
183
|
+
}
|